一、前言
JVM
性能优化步骤:
-
预估系统参数
-
压测后,调整
JVM
参数 -
线上系统监控和优化
-
统一的
JVM
参数模板
线上频繁 Full GC
的表现:
-
机器
CPU
负载过高 -
频繁
Full GC
报警 -
系统无法处理请求或者处理过慢
频繁 Full GC
常见原因:
-
对象频繁进入老年代,频繁触发
Full GC
系统承载高并发请求,或处理数据量过大,导致
Young GC
频繁,每次Young GC
过后存活对象太多,内存分配不合理,Survivor
区域过小。 -
系统一次性加载过多数据进入内存,大对象直接入老年代,频繁触发
Full GC
-
内存泄漏,对象无法回收,一直占用在老年代里,频繁触发
Full GC
-
MetaSpace
(永久代)加载类过多,触发Full GC
-
代码中使用
System.gc()
,触发Full GC
针对以上 Full GC
常见的原因,对应的优化方式:
-
jstat
分析,合理分配内存,调大Survivor
区域 -
dump
出内存快照,用MAT
工具进行分析,代码上排查 -
dump
出内存快照,用MAT
工具进行分析,代码上排查 -
若内存使用不多,还频繁触发
Full GC
,那么优化加载的类 -
若内存使用不多,还频繁触发
Full GC
,代码上排查,删除System.gc()
一、案例一:高分配速率( High Allocation Rate
)
分配速率( Allocation rate
)表示单位时间内分配的内存量。
通常使用 MB/sec
作为单位。上一次垃圾收集之后, 与下一次 GC
开始之前的年轻代使用量, 两者的差值除以时间, 就是分配速率。分配速率过高就会严重影响程序的性能, 在 JVM
中可能会导致巨大的 GC
开销。
-
正常系统: 分配速率较低 ~ 回收速率 -> 健康
-
内存泄漏: 分配速率 持续大于 回收速率 ->
OOM
-
性能劣化: 分配速率较高 ~ 回收速率 -> 亚健康
-
JVM
启动之后 291 ms, 共创建了 33,280 KB 的对象。第一次Minor GC
(小型GC
) 完成后, 年轻代中还有 5,088 KB 的对象存活。 -
在启动之后 446 ms, 年轻代的使用量增加到 38,368 KB , 触发第二次
GC
, 完成后年轻代的使用量减少到 5,120 KB。 -
在启动之后 829 ms, 年轻代的使用量为 71,680 KB,
GC
后变为 5,120 KB。
思考一个问题, 分配速率, 到底影响什么?
想一想, new
出来的对象, 在什么地方。
答案就是, Eden
。
假如我们增加 Eden
, 会怎么样。考虑蓄水池效应。最终的效果是, 影响 Minor GC
的次数和时间, 进而影响吞吐量。
在某些情况下, 只要增加年轻代的大小, 即可降低分配速率过高所造成的影响。
增加年轻代空间并不会降低分配速率, 但是会减少 GC
的频率。如果每次 GC
后只有少量对象存活, minor GC
的暂停时间就不会明显增加。
二、案例二:过早提升( Premature Promotion
)
提升速率( promotion rate
)用于衡量单位时间内从年轻代提升到老年代的数据量。
一般使用 MB/sec
作为单位, 和分配速率类似。
JVM
会将长时间存活的对象从年轻代提升到老年代。根据分代假设, 可能存在一种情况, 老年代中不仅有存活时间长的对象, 也可能有存活时间短的对象。
这就是过早提升: 对象存活时间还不够长的时候就被提升到了老年代。
major GC
不是为频繁回收而设计的, 但 major GC
现在也要清理这些生命短暂的对象, 就会导致 GC
暂停时间过长。这会严重影响系统的吞吐量。
GC
之前和之后的年轻代使用量以及堆内存使用量。
这样就可以通过差值算出老年代的使用量。
和分配速率一样, 提升速率也会影响 GC
暂停的频率。但分配速率主要影响 minor GC
, 而提升速率则影响 major GC
的频率。
有大量的对象提升, 自然很快将老年代填满。老年代填充的越快, 则 major GC
事件的频率就会越高。
一般来说过早提升的症状表现为以下形式:
-
短时间内频繁地执行
full GC
-
每次
full GC
后老年代的使用率都很低, 在 10-20% 或以下 -
提升速率接近于分配速率
要演示这种情况稍微有点麻烦, 所以我们使用特殊手段, 让对象提升到老年代的年龄比默认情况小很多。指定 GC
参数 -Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1
, 运行程序之后, 可以看到下面的 GC
日志:
解决这类问题, 需要让年轻代存放得下暂存的数据, 有两种简单的方法:
-
增加年轻代的大小, 设置
JVM
启动参数, 类似这样:-Xmx64m -XX:NewSize=32m
, 程序在执行时,Full GC
的次数自然会减少很多, 只会对minor GC
的持续时间产生影响。 -
减少每次批处理的数量, 也能得到类似的结果。至于选用哪个方案, 要根据业务需求决定。在某些情况下, 业务逻辑不允许减少批处理的数量, 那就只能增加堆内存, 或者重新指定年轻代的大小。如果都不可行, 就只能优化数据结构, 减少内存消耗。
但总体目标依然是一致的: 让临时数据能够在年轻代存放得下。