一、前言

​JVM​​ 性能优化步骤:

  1. 预估系统参数
  2. 压测后,调整JVM 参数
  3. 线上系统监控和优化
  4. 统一的JVM 参数模板

线上频繁 ​​Full GC​​ 的表现:

  • 机器CPU 负载过高
  • 频繁Full GC 报警
  • 系统无法处理请求或者处理过慢
频繁 ​​Full GC​​ 常见原因:
  1. 对象频繁进入老年代,频繁触发Full GC
    系统承载高并发请求,或处理数据量过大,导致Young GC 频繁,每次 Young GC 过后存活对象太多,内存分配不合理, Survivor 区域过小。
  2. 系统一次性加载过多数据进入内存,大对象直接入老年代,频繁触发Full GC
  3. 内存泄漏,对象无法回收,一直占用在老年代里,频繁触发Full GC
  4. ​MetaSpace​​ (永久代)加载类过多,触发 Full GC
  5. 代码中使用System.gc() ,触发 Full GC
针对以上 ​​Full GC​​ 常见的原因,对应的优化方式:
  1. ​jstat​​ 分析,合理分配内存,调大 Survivor 区域
  2. ​dump​​ 出内存快照,用 MAT 工具进行分析,代码上排查
  3. ​dump​​ 出内存快照,用 MAT 工具进行分析,代码上排查
  4. 若内存使用不多,还频繁触发Full GC ,那么优化加载的类
  5. 若内存使用不多,还频繁触发Full GC ,代码上排查,删除 System.gc()

一、案例一:高分配速率( ​​High Allocation Rate​​ )

分配速率( ​​Allocation rate​​ )表示单位时间内分配的内存量。

通常使用 ​​MB/sec​​ 作为单位。上一次垃圾收集之后, 与下一次 ​​GC​​ 开始之前的年轻代使用量, 两者的差值除以时间, 就是分配速率。分配速率过高就会严重影响程序的性能, 在 ​​JVM​​ 中可能会导致巨大的 ​​GC​​ 开销。

  • 正常系统: 分配速率较低 ~ 回收速率 -> 健康
  • 内存泄漏: 分配速率 持续大于 回收速率 ->OOM
  • 性能劣化: 分配速率较高 ~ 回收速率 -> 亚健康

JVM问题分析调优经验_加载

  1. ​JVM​​ 启动之后 291 ms, 共创建了 33,280 KB 的对象。第一次 Minor GC (小型 GC ) 完成后, 年轻代中还有 5,088 KB 的对象存活。
  2. 在启动之后 446 ms, 年轻代的使用量增加到 38,368 KB , 触发第二次GC , 完成后年轻代的使用量减少到 5,120 KB。
  3. 在启动之后 829 ms, 年轻代的使用量为 71,680 KB,GC 后变为 5,120 KB。

JVM问题分析调优经验_老年代_02

思考一个问题, 分配速率, 到底影响什么?

想一想, ​​new​​ 出来的对象, 在什么地方。

答案就是, ​​Eden​​ 。

假如我们增加 ​​Eden​​ , 会怎么样。考虑蓄水池效应。最终的效果是, 影响 ​​Minor GC​​ 的次数和时间, 进而影响吞吐量。

在某些情况下, 只要增加年轻代的大小, 即可降低分配速率过高所造成的影响。

增加年轻代空间并不会降低分配速率, 但是会减少 ​​GC​​ 的频率。如果每次 ​​GC​​ 后只有少量对象存活, ​​minor GC​​ 的暂停时间就不会明显增加。

二、案例二:过早提升( ​​Premature Promotion​​ )

提升速率( ​​promotion rate​​ )用于衡量单位时间内从年轻代提升到老年代的数据量。

一般使用 ​​MB/sec​​ 作为单位, 和分配速率类似。

​JVM​​ 会将长时间存活的对象从年轻代提升到老年代。根据分代假设, 可能存在一种情况, 老年代中不仅有存活时间长的对象, 也可能有存活时间短的对象。

这就是过早提升:  对象存活时间还不够长的时候就被提升到了老年代。

​major GC​​ 不是为频繁回收而设计的, 但 ​​major GC​​ 现在也要清理这些生命短暂的对象, 就会导致 ​​GC​​ 暂停时间过长。这会严重影响系统的吞吐量。

JVM问题分析调优经验_内存泄漏_03

​GC​​ 之前和之后的年轻代使用量以及堆内存使用量。

这样就可以通过差值算出老年代的使用量。

和分配速率一样, 提升速率也会影响 ​​GC​​ 暂停的频率。但分配速率主要影响 ​​minor GC​​ , 而提升速率则影响 ​​major GC​​ 的频率。

有大量的对象提升, 自然很快将老年代填满。老年代填充的越快, 则 ​​major GC​​ 事件的频率就会越高。

JVM问题分析调优经验_内存泄漏_04

一般来说过早提升的症状表现为以下形式:
  1. 短时间内频繁地执行full GC
  2. 每次full GC 后老年代的使用率都很低, 在 10-20% 或以下
  3. 提升速率接近于分配速率

要演示这种情况稍微有点麻烦, 所以我们使用特殊手段, 让对象提升到老年代的年龄比默认情况小很多。指定 ​​GC​​ 参数 ​​-Xmx24m -XX:NewSize=16m -XX:MaxTenuringThreshold=1​​ , 运行程序之后, 可以看到下面的 ​​GC​​ 日志:

JVM问题分析调优经验_加载_05

解决这类问题, 需要让年轻代存放得下暂存的数据, 有两种简单的方法:
  1. 增加年轻代的大小, 设置JVM 启动参数, 类似这样: -Xmx64m -XX:NewSize=32m , 程序在执行时, Full GC 的次数自然会减少很多, 只会对 minor GC 的持续时间产生影响。
  2. 减少每次批处理的数量, 也能得到类似的结果。至于选用哪个方案, 要根据业务需求决定。在某些情况下, 业务逻辑不允许减少批处理的数量, 那就只能增加堆内存, 或者重新指定年轻代的大小。如果都不可行, 就只能优化数据结构, 减少内存消耗。