➤ GC问题排查总结:
- GC常见问题: GC发生频繁, GC某个阶段耗时长
- 要定位问题, 首先看GC Log 和监控图, 确定如下:
- 现在用的是哪种GC收集器? ( 该收集器的GC的触发条件? 每个阶段做了什么? 哪个阶段有STW?)
- 如果是GC频繁, 是哪个分代的GC频繁?
- 如果是新生代GC: 新生对象太多, 新生代空间太小
- 如果是FGC: 有些FGC是因为新生代GC引起的
- 如果是GC某个阶段耗时过长, 需要确认: 这个阶段主要做了什么? 导致耗时变长的原因之一是扫描对象过多
➤ 案例1: Minor 和Major GC 频繁, Major耗时较长, 且出现晋升的动态年龄阈值被降低的情况
- 观察: 新生代和老年代使用率, 确定是因为大量新生对象(导致新生代GC频繁), JVM动态调整了晋升年龄, 导致大量低龄对象进入老年代.
- 分析: 如果新生代容量扩容1倍, 可以推算, MinorGC发生频率降低1倍, Minor GC的”扫描阶段”耗时增加1倍, 但”拷贝阶段”的耗时不会显著增加, 虽然Minor GC间隔增加, 但…
- 解决: 增大新生代容量
动态年龄计算:Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了 survivor 区的一半时,取这个年龄和 MaxTenuringThreshold 中更小的一个值,作为新的晋升年龄阈值。在本案例中,调优前:Survivor 区 = 64 M,desired survivor = 32 M,此时 Survivor 区中 age<=2 的对象累计大小为 41 M,41 M 大于 32 M,所以晋升年龄阈值被设置为 2,下次 Minor GC 时将年龄超过2的对象被晋升到老年代。
JVM 引入动态年龄计算,主要基于如下两点考虑:
- 如果固定按照 MaxTenuringThreshold 设定的阈值作为晋升条件: a)MaxTenuringThreshold 设置的过大,原本应该晋升的对象一直停留在 Survivor 区,直到 Survivor 区溢出,一旦溢出发生,Eden+Svuvivor 中对象将不再依据年龄全部提升到老年代,这样对象老化的机制就失效了。 b)MaxTenuringThreshold 设置的过小,“过早晋升”即对象不能在新生代充分被回收,大量短期对象被晋升到老年代,老年代空间迅速增长,引起频繁的 Major GC。分代回收失去了意义,严重影响 GC 性能。
- 相同应用在不同时间的表现不同:特殊任务的执行或者流量成分的变化,都会导致对象的生命周期分布发生波动,那么固定的阈值设定,因为无法动态适应变化,会造成和上面相同的问题。
➤ 案例2: 使用CMS, remark阶段STW超过1000ms
- 分析: remark是CMS进行老年代回收的第3个步骤, remark阶段需要以新生代对象为根进行扫描(防止跨代引用), 所以新生代对象数量直接影响remark阶段的扫描耗时
- CMS为了解决这个问题, 在remark阶段加入了一个”可中断的并发预清理阶段”(CMS-concurrent-abortable-preclean), 在”并发标记”阶段结束后, 如果Eden大小超过2M, 将会进入此阶段. 如果此阶段能等到一次Minor GC, 那么remark阶段需要扫描的对象数量将大大减少.
concurrent-abortable-preclean阶段是可中断的: 如果此阶段在超过
CMSMaxAbortablePrecleanTime
秒后都没有等到一次Minor, 仍旧会进入remark;案例中, 发现remark耗时过长的时候, 新生代使用率都很高, 所以可以判定remark耗时过长是因为新生代对象数量过多;
- 解决: CMS提供了
CMSScavengeBeforeRemark
参数, 强制在remark之前进行一次Minor GC;
➤ 案例3: 日志里Full GC耗时过长
分析: GC日志里的Full GC不代表全堆GC, 而是指有”STW的GC”类型,
引起STW 的GC可能原因: 1)新生代晋升时,发现老年代剩余空间不够; 2)老年代空间大于%; 3)Perm代发生GC;
- 案例: 排除1, 因为没有 promotion failed; 排除2, 因为发生Full GC时老年代使用率不高; 并且Full GC之后 Perm代变大, 确定是第3种情况, perm不够用;
- 解决: 增加 permSize