如何降低 young gc 时间
基础知识
young gc 主要采用的是 copying GC 算法;copying GC 算法主要有以下两个步骤:
Root Scanning
Object Copy
copying Gc 的执行过程大概是从 Gc roots 开始扫描其引用,扫描到的就是认为是存活的对象,其他的就是不需要的对象,然后把存放对象进行移动就 OK 了。
young gc 的耗时也基本上都在这两个步骤上。要想减少一次 young gc 的时间,必须想办法减少上面两步耗时。
根据官方文档可以知道,GC roots 包含以下引用:
所有 java 线程以及线程栈帧里指向 GC 堆里的对象的引用
JNI Local & Global
由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的
stack local Java 方法的 local 变量或参数
其他,包含 monitor & finalizable & native stack 等吧
Copying GC 算法最主要的特征就是它的 gc 时间只跟活对象的多少有关系,而跟它所管理的堆空间的大小没关系。
如何降低每次 young gc 的时间呢
从上面的分析可以知道只要减少 GC roots 集合大小以及降低每次 gc 之后的存活对象就可以了。
在 GC roots 中 跟业务方最相关的就是 java 线程,那要是把线程数减少是不是能降低 Root Scanning,进而降低整个 young gc 时间呢。
笔者负责的项目大部分项目都是采用 Hystrix 线程池作为超时熔断降级,因为依赖的下游接口很多很多并且很多时候需要分批,导致线程数特别多,高达 4000+,young gc 时 root scanning 占用了 15ms 左右,young gc 日志如下:
我采用了 Hystrix 信号量+RPC 异步化去改造项目,减少线程池数目。改造之后线程数在 700 左右,young gc 时 root scanning 占用的的时间 < 5ms。
降低 young gc 的总时间
调整 Eden 区域大小对应用产生的可能影响分析
相同的应用一般来说 gc roots 应该是保持不变的,可以简单认为 Root Scanning 相等(其实 live object 会影响到扫描时间,但是影响和 object copy 相比很小)
我们来看看 Object Copy 可能受到的影响(假设 Survivor 区域足够大,不会因为 copy 过程中 Survivor 不够大直接晋升到 old 区域)。
先看第一部分,Eden 移动到 Survivor 情况
假设机器 2 Eden 区域是 机器 1 的两倍大,其他条件都保持不变;
就一般情况来说(Survivor 区域中存活的对象比 Eden 少很多,比如 1%),那么机器 1 young gc 的频率是 机器 2 young gc 频率的 2 倍;那么假设机器 1 在 T 时间内 GC 一次,在 GC 之后由 Eden 区域晋升到 Survivor 的大小为 10M(即 age=1),那么机器 2 在 2T 时间之后发生 GC,1T-2T 之间生成的对象和机器 1 类似,GC 之后有 10M 进入 Survivor 区域,但是 0T-1T 内最多会剩余 10M 内存可能会进入到 Survivor,但是在经历 1T-2T 时间之后也有可能导致 object 已经不存活,如何判断这部分对象有没有存活呢,在机器 1 在 2T 的时间点要又要进行一次 young gc,那么在 0T-1T 之前存活的对象也就是 age=1 的对象将会再次会经历一次 young gc,便是了 age=2,所有看 age=2 的年龄段剩余多少就可以了。机器 2 一次 GC 之后,由 Eden 区域进入到 Survivor 区域中的大约等于 10M+机器 1 中 Survivor 中(age=2)也就是机器 1:age1+age2 中的 object 对象。
总结 机器 2 由 Eden 区域移动到 Survivor 的量就是机器 1 age1 + age2 的量。
第二部分的分析逻辑和第一部分的差不多,逻辑自己推。
结论:如果 age1 远大于 age2 中的值,那么调大 Eden 区域对减少 young gc 次数会很明显,并且每次 young gc time 时间变化不大,能明显降低 young gc 总体时间。
为了验证上面的理论分析,笔者找了一个 young gc 之后 age1>>>age2 的项目,young gc 日志如下:
笔者把 dx17 的 young 区域调大 (调整之后 Eden 为 1677824K),dx14 的 Eden 为 921600K,调整前后的 gc 时间如下:
从上面 3 张图可以看出 整体每分钟 young gc 时间由 125ms —>70ms,young gc 次数由 每分钟 12.7 —> 7,每次 young gc 的时间仍旧是 9.4 左右。用 awk 做了一下统计发现每次 young gc 之后的 live object 的大小由 2.85M 增加到了 3.3M。
减少对象生成 以到达降低 young gc 次数
尽量使用小对象,并且在方法内和线程内分配对象,利用 JIT 在优化时对象在栈上分配,减少在堆上分配内存,可以参考浅谈 HotSpot 逃逸分析,但是笔者在关闭逃逸分析的时候(-XX:-DoEscapeAnalysis),对线上机器,对 GC 请求没啥影响,但是自己写测试确实有比较大的影响,没有明白为什么。
使用对象池,减少对象产生。
看完三件事❤️
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
关注公众号 『 java 烂猪皮 』,不定期分享原创知识。
同时可以期待后续文章 ing🚀
作者:朱纪兵
出处:https://club.perfma.com/article/604033
评论