介绍
? ? ? ? 有一个系统,有如下特征,偶尔会触发 FGC(1小时几次,每次持续4~5分钟):
- 机器规格 48C96G,规格已经很大了,不宜再扩大
- 内存分配:Young 20GB(1:1:8), Old? 70GB, 堆外4GB, 预留 2GB 给 OS
- 使用 ParNew GC +? CMS GC
- 启动后需要加载大量元数据、缓存,大概占据 30GB~40GB 内存,这些元数据、缓存常驻内存
- 业务繁忙,堆内存分配速度很快,低峰期 1GB/s+,高峰期 5GB/s+
- 单机大部分时间不会FGC,Old区使用率也符合预期?(比例在30GB~40GB除以70GB);但即将发生FGC时,Old区的利用率是在短时间(2~3分钟以内)猛涨上去的,不是慢慢涨上去的
- 在不改代码的情况下,尝试过调整如下 GC 参数,效果不明显或者恶化
- 调大 Old、调小 Eden
- 使用 G1 GC
- 调整 CMS 开始的阈值(从68%改成45%)
- 每次执行完 FGC 后 Old 区使用率确实可以降下来,说明没有内存泄露
? ? ? ? 我们希望能尽可能减少?FGC 发生频率,GC STW < 5 秒都能接受。
常见 FGC 原因
- 有内存泄露
- 申请超过容量的堆内存
- 碎片化严重 + 大对象
- Survivor 太小,YGC 后发生提前晋升,再加上 Old区的垃圾回收速度 < Old区的垃圾产生速度最终导致 FGC
? ? ? ? 原因 1 属于代码问题,应该很好看出此类现象,GC 算法无能为力。
? ? ? ? 原因 2?属于代码问题,瞬间申请的内存超过了总容量,GC 算法无能为力,可以考虑调低系统的并发度,否则最终系统会由于 OOM 挂掉。
? ? ? ? 原因 3?属于 GC 算法问题,一个好的 GC 算法应该要能减轻此类问题,对此程序员不好做些什么改进。
? ? ? ? 原因 4?是我们系统本次遇到的原因,下面我展开介绍。
GC提前晋升/过早晋升
? ? ? ??提前晋升是指如下流程的第3步:
- Java 执行到分配对象的代码
- Eden 区容量不足触发 YGC
- YGC 完成之后,Survivor 区容不下在本次 YGC 里存活下来的所有对象,此时部分对象就会提前晋升到 Old 区
提前晋升的危害
? ? ? ? Young 区 和 Old 区的回收算法不同,导致回收速度有很大差异,Old 区的垃圾回收速度是很慢的。如果你的系统里 Old 区的垃圾产生速度 > Old 区的垃圾回收速度,那么就很有可能出现 “concurrent mode failure” 引起?FGC。提前晋升会显著增加 “Old 区的垃圾产生速度”,因此更容易出现 FGC。
对象的声明周期长短
? ? ? ? 一般来说我们认为:
- 短生命周期对象应该分配到 Young 区,它们一般在几次 YGC 内就必须结束生命。
- 长生命周期对象应该进入到 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。
? ? ? ? 按这个思路,我们的优化是:
- 降低系统的并发度:这个优化在FGC之前就已经做了,我们限制最多有 16 个 RPC 请求同时在执行,不宜过低,过低可能就会出现明显的积压了(根据业务需求)
- 调大 Survivor 区,Survivor 默认是占 Young 区的 1/(1+1+8)=1/10=10%=2GB,将其调整到 1/(1+1+2)=25%=5GB 之后效果非常好,提前晋升到 Old 区的对象肉眼可见的变少了(可以通过看 Old 区使用量的监控曲线图);事实上我们可以适当将 Old 区的容量更多地划分到 Survivor 上效果会更好。
- 在执行 RPC 请求的过程中,将那些不会被引用的变量设置为null;在 callback 里如果要打印 request.size() 的需要将 request.size() 的值提前保存为一个 int 再在回调里引用,避免回调直接依赖 request 变量。
将变量设置为null有利于加速 GC 吗?
? ? ? ? 先说结论:至少不会更差,在某些场景下能显著优化GC
? ? ? ? TODO 下面放一些验证代码
????????