java虚拟机在可用内存空间减少时,会增加垃圾回收的次数,而垃圾回收是需要消耗时间的,会出现短暂的停顿(STW,stop the world)现象,如果频繁的进行垃圾回收,特别是老年代的垃圾回收,则会影响到请求的响应时间。
1. 现象
以下是一个项目中使用jdk8默认参数运行时,jmap -heap 内存使用情况图。可以看到 Survivor 区使用占比 71%,在比较高的水平。
仔细观察这张图,其中包含几个重要信息:
- From 和 To 区都比较小,只有 2M。容量比较小,才显得占比高。
- Old 区的占比和使用量都比较高。
此外,还可以看到 Eden、From、To 之间的比例不是默认的 8:1:1。于是,立马就想到 AdaptiveSizePolicy。
2. 自适应大小策略AdaptiveSizePolicy
AdaptiveSizePolicy(自适应大小策略) 是 JVM GC Ergonomics(自适应调节策略) 的一部分。
如果开启 AdaptiveSizePolicy,则每次 GC 后会重新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程中统计的 GC 时间、吞吐量、内存占用量。
JDK 1.8 默认使用 UseParallelGC 垃圾回收器,该垃圾回收器默认启动了 AdaptiveSizePolicy。
AdaptiveSizePolicy 有三个目标:
- Pause goal:应用达到预期的 GC 暂停时间。
- Throughput goal:应用达到预期的吞吐量,即应用正常运行时间 / (正常运行时间 + GC 耗时)。
- Minimum footprint:尽可能小的内存占用量。
AdaptiveSizePolicy 为了达到三个预期目标,涉及以下操作:
- 如果 GC 停顿时间超过了预期值,会减小内存大小。理论上,减小内存,可以减少垃圾标记等操作的耗时,以此达到预期停顿时间。
- 如果应用吞吐量小于预期,会增加内存大小。理论上,增大内存,可以降低 GC 的频率,以此达到预期吞吐量。
- 如果应用达到了前两个目标,则尝试减小内存,以减少内存消耗。
AdaptiveSizePolicy 看上去很智能,但有时它也很调皮,会引发 GC 问题。
通过jstat -gcutil命令查看垃圾回收情况,如果出现了频繁的垃圾回收(比如每几分钟就存在一次FGC),而且垃圾回收时间长(比如FGC超过200ms),则有比较对默认策略进行调整。
平均停顿时间超过200ms,对于高 QPS 应用的影响是明显的。
3. 解决方案
3.1 显示设置内存分配比例
保持使用 UseParallelGC,显式设置 -XX:SurvivorRatio=8。
配置参数进行测试:
看到默认配置下,三者之间的比例不是 8:1:1。
可以看到,加上参数 -Xmn100m -XX:SurvivorRatio=8 参数后,固定了 Eden 和 Survivor 之间的比例。
这里的参数根据具体环境进行调整。
3.2 使用 CMS 垃圾回收器
使用 CMS 垃圾回收器。CMS 默认关闭 AdaptiveSizePolicy。
配置参数 -XX:+UseConcMarkSweepGC,通过 jinfo 命令查看,可以看到 CMS 默认减去/不使用 AdaptiveSizePolicy。
可以看出,Eden 和 Survivor 之间的比例被固定,To 区没有被缩小。老年代的使用量和使用率也都很正常。
4. 问题小结
- 使用 JDK 1.8,默认垃圾回收器是Parallel Scavenge,并且默认开启 AdaptiveSizePolicy;
- AdaptiveSizePolicy 动态调整 Eden、Survivor 区的大小,存在将 Survivor 区调小的可能。当 Survivor 区被调小后,部分 YGC 后存活的对象直接进入老年代。老年代占用量逐渐上升从而触发 FGC,导致较长时间的 STW;
- 建议使用 CMS 垃圾回收器,默认关闭 AdaptiveSizePolicy;
- 建议在 JVM 参数中加上 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution,让 GC log 更加详细,方便定位问题;
- 在大内存(比如8GB及以上)和高QPS的情况下,确保快速响应时间,可以考虑使用G1垃圾回收器进行垃圾回收,G1垃圾回收期可以每次收集部分垃圾来满足小的停顿时间要求;