记CMS FGC 的一次调优

介绍

        有一个系统,有如下特征,偶尔会触发 FGC(1小时几次,每次持续4~5分钟):

  1. 机器规格 48C96G,规格已经很大了,不宜再扩大
  2. 内存分配:Young 20GB(1:1:8), Old  70GB, 堆外4GB, 预留 2GB 给 OS
  3. 使用 ParNew GC +  CMS GC
  4. 启动后需要加载大量元数据、缓存,大概占据 30GB~40GB 内存,这些元数据、缓存常驻内存
  5. 业务繁忙,堆内存分配速度很快,低峰期 1GB/s+,高峰期 5GB/s+
  6. 单机大部分时间不会FGC,Old区使用率也符合预期 (比例在30GB~40GB除以70GB);但即将发生FGC时,Old区的利用率是在短时间(2~3分钟以内)猛涨上去的,不是慢慢涨上去的
  7. 在不改代码的情况下,尝试过调整如下 GC 参数,效果不明显或者恶化
    1. 调大 Old、调小 Eden
    2. 使用 G1 GC
    3. 调整 CMS 开始的阈值(从68%改成45%)
  8. 每次执行完 FGC 后 Old 区使用率确实可以降下来,说明没有内存泄露

        我们希望能尽可能减少 FGC 发生频率,GC STW < 5 秒都能接受。

常见 FGC 原因

  1. 有内存泄露
  2. 申请超过容量的堆内存
  3. 碎片化严重 + 大对象
  4. Survivor 太小,YGC 后发生提前晋升,再加上 Old区的垃圾回收速度 < Old区的垃圾产生速度最终导致 FGC

        原因 1 属于代码问题,应该很好看出此类现象,GC 算法无能为力。

        原因 2 属于代码问题,瞬间申请的内存超过了总容量,GC 算法无能为力,可以考虑调低系统的并发度,否则最终系统会由于 OOM 挂掉。

        原因 3 属于 GC 算法问题,一个好的 GC 算法应该要能减轻此类问题,对此程序员不好做些什么改进。

        原因 4 是我们系统本次遇到的原因,下面我展开介绍。

GC提前晋升/过早晋升

        提前晋升是指如下流程的第3步:

  1. Java 执行到分配对象的代码
  2. Eden 区容量不足触发 YGC
  3. YGC 完成之后,Survivor 区容不下在本次 YGC 里存活下来的所有对象,此时部分对象就会提前晋升到 Old 区

提前晋升的危害

        Young 区 和 Old 区的回收算法不同,导致回收速度有很大差异,Old 区的垃圾回收速度是很慢的。如果你的系统里 Old 区的垃圾产生速度 > Old 区的垃圾回收速度,那么就很有可能出现 “concurrent mode failure” 引起 FGC。提前晋升会显著增加 “Old 区的垃圾产生速度”,因此更容易出现 FGC。

对象的声明周期长短

        一般来说我们认为:

  1. 短生命周期对象应该分配到 Young 区,它们一般在几次 YGC 内就必须结束生命。
  2. 长生命周期对象应该进入到 Old 区,这些对象一般是“长期”存在的,比如 static 级别的变量、整个 Spring 的 context 及其引用的 beans、配置过期时间为10分钟的cache等。

        在实际中偶尔会有中生命周期对象出现。

我自创的一些术语,大家能get到含义就行,至于叫法之后可以改改

        主观上它们其实是属于短生命周期对象,但由于各种原因它们撑过了好几次 YGC,最终由于年龄到了或者 Survivor 满了晋升到 Old 区。

        比如在我们的系统里需要经常向外发送 RPC 请求,请求体经 JSON 序列化之后大小几十MB~几百MB都有,为了构造这个请求体本身也需要很多中间对象,它们也是要占据内存的。如果 RPC 发生了超时了,那么这些中间对象不得不等到 RPC 回调执行才会被释放(因为在回调里还用到它们了)。

分析和优化

        我们认为我们系统发生 FGC 的直接原因是,某一时刻发爆发地执行大量 RPC 请求由于 RPC 需要等待较长时间才会报超时异常导致内存无法被及时回收,YGC 频率高(高峰时期大概3~4秒就需要一次 YGC),每个请求消耗的内存多,累加起来已经超过了 Survivor 区(当时我们的 Survivor 区大小才 2GB),导致不停有对象提前晋升到 Old 区,晋升速度超过 Old 区 GC 回收速度,引发 FGC。

        按这个思路,我们的优化是:

  1. 降低系统的并发度:这个优化在FGC之前就已经做了,我们限制最多有 16 个 RPC 请求同时在执行,不宜过低,过低可能就会出现明显的积压了(根据业务需求)
  2. 调大 Survivor 区,Survivor 默认是占 Young 区的 1/(1+1+8)=1/10=10%=2GB,将其调整到 1/(1+1+2)=25%=5GB 之后效果非常好,提前晋升到 Old 区的对象肉眼可见的变少了(可以通过看 Old 区使用量的监控曲线图);事实上我们可以适当将 Old 区的容量更多地划分到 Survivor 上效果会更好。
  3. 在执行 RPC 请求的过程中,将那些不会被引用的变量设置为null;在 callback 里如果要打印 request.size() 的需要将 request.size() 的值提前保存为一个 int 再在回调里引用,避免回调直接依赖 request 变量。

将变量设置为null有利于加速 GC 吗?

        先说结论:至少不会更差,在某些场景下能显著优化GC

        TODO 下面放一些验证代码

        

你可能感兴趣的:(java,开发语言,GC,FGC,CMS)