JVM 调优
优化策略
内存优化策略
高并发业务场景下,应增加新生代的大小
Minor GC 时间 = T1(扫描新生代)+ T2(复制存活对象)
在 JVM 中,复制对象的成本要远高于扫描成本。如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。
新生代大小选择
- 响应时间优先的应用:尽可能设大
- 尽可能设大, 直到接近系统的最低响应时间限制(根据实际情况选择)。 在此种情况下, 新生代收集发生的频率也是最小的。同时, 减少到达老年代的对象。
- 吞吐量优先的应用:尽可能设大
- 尽可能的设置大,可能到达 Gbit 的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合 8CPU 以上的应用
避免设置过小:
- MinorGC 次数更加频繁
- 可能导致 MinorGC 对象直接进入老年代,如果此时老年代满了,会触发 FullGC.
老年代大小选择
- 响应时间优先的应用
- 老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数。如果堆设置小了,可能会造成内存碎片,高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间
- 参考参数:并发垃圾收集信息、持久代并发收集次数、传统 GC 信息、花在新生代和老年代回收上的时间比例
- 吞吐量优先的应用:很大的新生代和一个较小的老年代
- 尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象。
GC 优化策略
GC 性能衡量指标:
- 吞吐量:不能低于 95%
- 这里的衡量吞吐量是指应用程序所花费的时间和系统总运行时间的比值。
- GC 的吞吐量:系统总运行时间 = 应用程序耗时 + GC 耗时
- 如果系统运行了 100 分钟,GC 耗时1 分钟,则系统吞吐量为 99%。
- 停顿时间
- 垃圾回收频率
- 通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间
- 所以我们需要适当地增大堆内存空间,保证正常的垃圾回收频率即可。
策略
- 降低 Minor GC 频率:增大新生代空间来降低 Minor GC 的频率
- 降低 Full GC 频率
- 减少创建大对象
- 增大堆内存空间,设置初始化堆内存为最大堆内存
- 选择合适的 GC 回收器
- 吞吐量优先:ParallerGC
- 响应时间优先
- 小于 6G:CMS
- 大于 6G:G1;大于 8 G,必须选 G1
JVM 预调优
调优目的:
- 优化 JVM 运行环境(慢、卡顿等):CPU 占用过高和内存占用过高
- 解决 JVM 中的问题(OOM 等)
无监控不优化:做压力测试
这里的监控指的是压力测试,能够看到结果,有数据体现的,不要用感觉去优化,所有的东西一定要有量化的指标,比如吞吐量,响应时间,服务器资源,网络资源等等。
步骤
- 设定业务场景:现在都是微服务架构了,服务拆分出来以后更加适合做场景设定
- 吞吐量
- 响应时间
- 计算内存需求
- 内存不是越大越好,对于一般系统来说,内存的需求是弹性的,没有固定的规范。内存小,回收速度快也能承受
- 虚拟机栈的大小在高并发情况下可以变小
- 元空间(方法区)保险起见还是设定一个最大的值(默认情况下元空间是没有大小限制的),一般限定几百M 就够用了,为什么说还限定元空间:
举例子:一台 8G 的内存的服务器,如果运行时还有其他的程序加上虚拟机栈加上元空间,占用超过 6 个 G 的话,那么我们设定堆是弹性的(max=4G),那么其实堆空间拓展也超不过 2G,所以这个时候限制元空间还是有必要的。
- 选定 CPU:看实际物理机的情况,不能单看虚拟化后的参数指标
- 选择合适的垃圾回收器
- 吞吐量优先:ParallelGC
- 响应时间优先:G1(大于 6G 直接选 G1) 和 CMS
- 设定新生代大小、分代年龄
- 吞吐量优先:一个很大的新生代和一个较小的老年代
- 这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽存放长期存活对象
- 响应时间优先:新生代尽可能设大,老年代要仔细分析后再选择合适的大小
- 吞吐量优先:一个很大的新生代和一个较小的老年代
- 设定日志参数
- 只有一个日志文件肯定不行,有时候一个高并发项目一天产生的日志文件就上 T,其实记录日志这个事情,应该是运维干的事情。日志文件帮助我们分析问题
高并发电商系统 JVM 调优示例
GC 预估:分析 GC 的原因
如果是普通业务,一般处理时间比较平缓,大概在 3,4 个小时处理,算出来每秒只有几十单,这个一般的应用可以处理过来(不需要 JVM 预估调优)。另外电商系统中有大促场景(秒杀、限时抢购等),一般这种业务是几种在几分钟。我们算出来大约每秒 2000 单左右的数据,承受大促场景的使用 4 台服务器(使用负载均衡)。每台订单服务器也就是大概 500 单/秒,我们测试发现,每个订单处理过程中会占据 0.2MB 大小的空间(什么订单信息、优惠券、支付信息等等),那么一台服务器每秒产生 100M 的内存空间,这些对象基本上都是朝生夕死,也就是1 秒后都会变成垃圾对象。
GC 预估调优
-Xms3072M -Xmx3072M -Xmn2048M -XX:SurvivorRatio=7
调大新生代-Xss256K -XX:MetaspaceSize= 128M -XX:MaxMetaspaceSize= 128M
限制虚拟机栈和元空间-XX:MaxTenuringThreshold=2
缩小分代年龄,便于 Spring 对象进入老年代-XX:ParallelGCThreads=8
用 ParallelGC 的话,可以设置多线程 GCCMS/ G1
响应时间优先
JVM 调优
CPU 占用过高
如果 CPU 的 100%,要从两个角度出发:
- 业务线程疯狂运行,比如死循环。
- GC 线程在疯狂的回收,因为 JVM 中垃圾回收器主流也是多线程的,所以很容易导致 CPU 的100%
排查步骤
- 先通过
top
命令找到消耗 cpu 很高的进程 id - 执行
top -p pid
单独监控该进程 - 在
top
监控界面输入H
,获取当前进程下的所有线程信息 - 找到消耗 cpu 特别高的线程编号
- 执行
jstack pid
对当前的进程做 dump,输出所有的线程信息 - 将第 4 步得到的线程编号转成 16 进制是 0x.....
- 根据第 6 步得到的 0x...... 在第 5 步的线程信息里面去找对应线程内容
- 解读线程信息,定位具体代码位置
- 如果是 VM Thread,一般是 GC 导致的,使用
jstat -gc pid 250 10
监控,进行 GC 调优
- 如果是 VM Thread,一般是 GC 导致的,使用
内存占用过高
在遇到内存溢出的问题的时候,一般情况下我们要查看系统中哪些对象占用得比较多,在业务代码中,找到对应的对象,分析对应的类,找到为什么这些对象不能回收的原因
排查步骤
jmap –histo pid | head -20
内存占用最多的 20 个类- JVM 内存泄漏可以使用 MAT 进行分析
- 如果一通分析下来啥问题也没有发现,就应该是直接内存泄漏
- 定位代码
优化思路:
- 程序优化,效果通常非常大
- 扩容,如果金钱的成本比较小,不要和自己过不去
- 参数调优,在成本、吞吐量、延迟之间找一个平衡点
常见问题分析
- 超大对象
- 代码中创建了很多大对象, 且一直因为被引用不能被回收,这些大对象会进入老年代,导致内存一直被占用,很容易引发 GC 甚至是 OOM
- 超过预期访问量
- 通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
- 比如如果一个系统高峰期的内存需求需要 2 个G 的堆空间,但是堆空间设置比较小,导致内存不够,导致 JVM 发起频繁的 GC 甚至 OOM。
- 过多使用 Finalizer
- 过度使用终结器(Finalizer),对象没有立即被 GC,Finalizer 线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的 CPU 时间较少,因此它永远也赶不上主线程的步伐,程序消耗了所有的可用资源,最后抛出 OutOfMemoryError 异常。
- 内存泄漏:大量对象引用没有释放,JVM 无法对其自动回收;程序在申请内存后,无法释放已申请的内存空间。
- 长生命周期的对象持有短生命周期对象的引用
- 将 ArrayList 设置为静态变量,则容器中的对象在程序结束之前将不能被释放
- 连接未关闭:如数据库连接、网络连接和 IO 连接等,只有连接被关闭后,垃圾回收器才会回收对应的对象
- 变量作用域不合理
- 一个变量的定义的作用范围大于其使用范围
- 没有及时地把对象设置为
null
- 内部类持有外部类
- Java 的非静态内部类的这种创建方式,会隐式地持有外部类的强引用
- 解决方案:在内部类的内部显示持有一个外部类的软引用(或弱引用),并通过构造方法的方式传递进来,在内部类的使用过程中,先判断一下外部类是否被回收
- Hash 值改变:比如无法删除集合中的元素
- 在集合中,如果修改了对象中的那些参与计算哈希值的字段,会导致无法从集合中单独删除当前对象,造成内存泄露
- 用数组做栈,出栈时记得把数组对应下标元素置为
null
- 代码问题:每次进行 FullGC 发现堆空间回收的比例比较小,尤其是老年代,同时对象越来越多
- 长生命周期的对象持有短生命周期对象的引用
- 内存泄漏和内存溢出:内存溢出往往是内存泄漏造成的
- 内存溢出:实实在在的内存空间不足导致;检查代码以及设置足够的空间
- 内存泄漏:一定是代码有问题,该释放的对象没有释放,常见于使用容器保存元素的情况下
直接内存泄漏分析
排查步骤
- top 命令追踪内存使用情况
- VIRT:virtual memory usage,申请的
- 进程“需要的”虚拟内存大小,包括进程使用的库、代码、数据等
- 假如进程申请100m 的内存,但实际只使用了10m,那么它会增长100m,而不是实际的使用量
- RES:resident memory usage ,实际使用的
- 如果申请 100m 的内存,实际使用 10m,它只增长10m,与 VIRT 相反
- VIRT:virtual memory usage,申请的
jmap –heap
,查看 JVM 内存使用情况jstack
,查看线程多少,判断虚拟机栈使用内存的情况jmap -histo 3468 | head -20
,查看占用内存最多的对象- 把内存 dump 下来,放到 MAT 中进行分析
- 如果上述排查都没发现问题,应该是发生了直接内存泄漏
可以使用 NMT(Native Memory Tracking) 追踪 Native 内存的使用情况:
-XX:NativeMemoryTracking=detail
启用jcmd $pid VM.native_memory summary
查看内存分配
NMT 是 Hotspot VM 用来分析 VM 内部内存使用情况的一个功能,可以用 jcmd(jdk 自带)来访问 NMT 的数据。打开 NMT 会带来 5%-10%的性能损耗。
具体排查可以使用 perf,这超出了一般 java 程序员的范畴:yum install perf
JVM 参数建议
生产服务器推荐开启:
-XX:-HeapDumpOnOutOfMemoryError
默认关闭,建议开启,OOM 时,输出一个 dump 文件,记录当时的堆内存快照-XX:HeapDumpPath=./java_pid<pid>.hprof
用来设置堆内存快照的存储文件路径,默认是 java 进程启动位置
调优之前开启、调优之后关闭:
-XX:+PrintGC
-XX:+PrintGCDetails, +XX:+PrintGCTimeStamps
-Xlogger:logpath
设置 gc 的日志路径-XX:+PrintHeapAtGC
打印推信息,获取 Heap 在每次垃圾回收前后的使用状况-XX:+TraceClassLoading
在系统控制台信息中看到 class 加载的过程和具体的 class 信息,可用以分析类的加载顺序以及是否可进行精简操作
其他参数:
-XX:+DisableExplicitGC
禁止在运行期显式地调用System.gc()