一、背景
2021年2月,收到反馈,视频APP某核心接口高峰期响应慢,影响用户体验。
通过监控发现,接口响应慢主要是P99耗时高引起的,怀疑与该服务的GC有关,该服务典型的一个实例GC表现如下图:
可以看出,在观察周期里:
- 平均每10分钟Young GC次数66次,峰值为470次;
- 平均每10分钟Full GC次数0.25次,峰值5次;
可见Full GC非常频繁,Young GC在特定的时段也比较频繁,存在较大的优化空间。由于对GC停顿的优化是降低接口的P99时延一个有效的手段,所以决定对该核心服务进行JVM调优。
二、优化目标
- 接口P99时延降低30%
- 减少Young GC和Full GC次数、停顿时长、单次停顿时长
由于GC的行为与并发有关,例如当并发比较高时,不管如何调优,Young GC总会很频繁,总会有不该晋升的对象晋升触发Full GC,因此优化的目标根据负载分别制定:
目标1:高负载(单机1000 QPS以上)
- Young GC次数减少20%-30% ,Young GC累积耗时不恶化;
- Full GC次数减少50%以上,单次、累积Full GC耗时减少50%以上,服务发布不触发Full GC。
目标2:中负载(单机500-600)
- Young GC次数减少20%-30% ,Young GC累积耗时减少20%;
- Full GC次数不高于4次/天,服务发布不触发Full GC。
目标3:低负载(单机200 QPS以下)
- Young GC次数减少20%-30% ,Young GC累积耗时减少20%;
- Full GC次数不高于1次/天,服务发布不触发Full GC。
三、当前存在的问题
当前服务的JVM配置参数如下:
-Xms4096M -Xmx4096M -Xmn1024M -XX:PermSize=512M -XX:MaxPermSize=512M
单纯从参数上分析,存在以下问题:
未显示指定收集器
JDK 8默认搜集器为ParrallelGC,即Young区采用Parallel Scavenge,老年代采用Parallel Old进行收集,这套配置的特点是吞吐量优先,一般适用于后台任务型服务器。
比如批量订单处理、科学计算等对吞吐量敏感,对时延不敏感的场景,当前服务是视频与用户交互的门户,对时延非常敏感,因此不适合使用默认收集器ParrallelGC,应选择更合适的收集器。
Young区配比不合理
当前服务主要提供API,这类服务的特点是常驻对象会比较少,绝大多数对象的生命周期都比较短,经过一次或两次Young GC就会消亡。
再看下当前JVM配置:
整个堆为4G,Young区总共1G,默认-XX:SurvivorRatio=8,即有效大小为0.9G,老年代常驻对象大小约400M。
这就意味着,当服务负载较高,请求并发较大时,Young区中Eden + S0区域会迅速填满,进而Young GC会比较频繁。
另外会引起本应被Young GC回收的对象过早晋升,增加Full GC的频率,同时单次收集的区域也会增大,由于Old区使用的是ParralellOld,无法与用户线程并发执行,导致服务长时间停顿,可用性下降, P99响应时间上升。
未设置
-XX:MetaspaceSize和-XX:MaxMetaspaceSize
Perm区在jdk 1.8已经过时,被Meta区取代, 因此-XX:PermSize=512M -XX:MaxPermSize=512M配置会被忽略, 真正控制Meta区GC的参数为 -XX:MetaspaceSize: Metaspace初始大小,64位机器默认为21M左右 -XX:MaxMetaspaceSize: Metaspace的最大值,64位机器默认为18446744073709551615Byte, 可以理解为无上限 -XX:MaxMetaspaceExpansion: 增大触发metaspace GC阈值的最大要求 -XX:MinMetaspaceExpansion: 增大触发metaspace GC阈值的最小要求,默认为340784Byte
这样服务在启动和发布的过程中,元数据区域达到21M时会触发一次Full GC (Metadata GC Threshold),随后随着元数据区域的扩张,会夹杂若干次Full GC (Metadata GC Threshold),使服务发布稳定性和效率下降。
此外如果服务使用了大量动态类生成技术的话,也会因为这个机制产生不必要的Full GC (Metadata GC Threshold)。