前言:
上一篇我们分享了线上应用的 JVM 内存预估技巧,通过对 JVM 内存的预估来合理的选用服务器资源和应用部署方案,本篇我们来分享引用的 JVM 调优实例,如何通过 JVM 调优来降低应用的 GC 频率。
JVM 系列文章传送门
JVM 性能调优 – 线上应用 JVM 内存的的预估设置【实战】
业务案例
假设我们有一个日处理数据过亿的数据计算系统,需要持续不断地从各个数据存储引擎查询数据到 Java 应用中进行归档计算,为了理解简单,我们将案例简单化,数据处理模型大致如下:
上述的业务模型可以简单理解为一个数据清洗系统,这个数据清洗系统预计每分钟要执行 1000 次数据清洗任务,当然这肯定是一个分布式部署的系统,单落到每个应用上的任务大概是每分钟执行 100 次左右的数据清洗任务。
假设每次清洗任务需要处理 10000 条数据,每次任务的执行需要消耗 10 秒的时间,当前每台服务器的配置是 4C 8G 的配置,分到 JVM 堆的内存是 4.2G,JVM 参数配置是默认配置。
以上就是我们的业务模型,后面我们将根据这个业务模型来完成 JVM 调优。
当前 JVM 堆内存结构分析
上面我们提到了 JVM 参数是默认的参数,因此新生代和老年代分别占有内存为 1.4G 和 2.8G,新生代中 Eden 区域和 Survivor From 和 Survivor To 区域分别占有内存 1.12G 和 0.14G、0.14G,内存占用模型如下:
以上就是默认的内存占用情况。
当前 JVM 堆的 GC 情况分析
前面我们分析了单个应用上的任务大概是每分钟执行 100 次左右的数据清洗任务,而每次的清洗对象是 10000 条数据,每次的清洗时间是 10 秒,我们假设每条数据的数据占用的内存较大为1KB,那么每次清洗任务需要占用 50000KB,大概是 10MB 的内存,也就是每次执行任务会在 JVM 堆中的 Eden 区域占用 50MB 的内存空间。
因为每分钟要执行 100 次任务,也就是每分钟需要在 Eden 区域占用 1000MB 空间,也就是大概 1G 的空间,而我们的 Eden 区域上面已经分析过了总内存为 1.12GB,加上系统运行中其他边缘业务产生的对象,基本就是 1 分钟就把 Eden 区域的空间占满了,此时就是要进行 Minor GC 了。
Minor GC 过程分析
假设在 1 分钟过后,Eden 区域内存使用完了,再进行下一个数据清洗任务的时候,就会触发 Minor GC。
前面在聊 JVM GC 过程中有提到,Minor GC 的第一步是判断老年代的空间是否大于年轻代的内存,如果不大于则直接触发 Full GC,显然我们这一次 GC 是绝对安全的,因为我们的老年代有 2.8G 的空间。
此次 Minor GC 可以回收多少对象呢?
Minor GC 可以回收多少对象取决于还有多少个任务没有执行完毕,我们假设此时还有 20 个任务没有执行完毕,也就是还有 200 MB 的底对象是不能够回收的,能够回收的是 700MB 的对象,正常来说这 200 MB 对象要去到 Survivor 区域的,但是我们上面分析了,我们的 Survivor 区域只有 0.14GB 也就是 140MB 左右的空间,这 200 MB 的对象显然放不下,此时就会通过空间担保机制, JVM 就会把这 200MB 的对象存放到老年代,这就是这一次 Minor GC 的过程。
一次 Minor GC 会晋升 200MB 的对象到老年代,而前面分析的每分钟都要进行一次 Minor GC,我们的老年代内存空间只有 2.8G,也就是第15 次 Minor GC 的时候老年代空间已经不足了,就会触发 Full GC了。
对象晋升老年代的时候,会先检查老年代可用空间是否大于新生代全部对象?此时老年代可用空间 200MB,新生代对象有 1.14GB,假设当前 Minor GC 过后新生代对象全部存活,老年代是放不下的,那么此时就得看 -XX:-HandlePromotionFailure 参数是否打开了,一般该参数都会打开,此时会进入第二步检查,会看老年代可用空间是否大于历次 Minor GC 过后进入老年代的对象的平均大小,前面已经计算过了,每次 Minor GC 大概会有 200MB 对象会进入老年代,因此到第 15 次 Minor GC 的时候,再通过这个判断过程的时候,发现都不满足,就会触发 Full GC,也就是15 分钟就会触发一次 Full GC,这个 Full GC 的频率十分之高了。
Minor GC 到 Full GC 过程演变如下图:
对频繁 Full GC 问题进行 JVM 调优
上面我们分析了如果按当前的方式进行运行,系统每 15 分钟就要发生一个 Full GC,这个 Full GC 的频率非常高了,我们知道 Full GC 是非常影响系统性能的,那么我们是否可以降低 Full GC 频率呢?
上面案例快速触发 Full GC 的原因是什么?
根据分析过程不难看出,迅速触发 Full GC 的根本原因是每次进行 Minor GC 的时候根本没有利用到 Survivor 区域,对象直接进入了老年代,那我们 JVM 调优的入口就来了,那就是利用上 Survivor 区域。
如何利用 Survivor 区域?
本案例中利用 Survivor 区域有两种方式,如下
- 调整 Eden 区域和 Survivor 区域比例,例如比例由默认的 8:1:1 调整为 6:2:2,也就是调整 -XX:SurvivorRatio=8 这个参数,本案例不适用,但也不失为一种 JVM 调优的手段。
- 调整年轻代和老年代的内存占比,例如由默认的 1:2 调整为 2:2,实际 JVM 调优过程中根据具体场景来进行选择。
本案例我们采用调整 JVM 的年轻代与老年代的内存占比来进行 JVM 调优,我们将默认的 1:2 的比例调整为 1:1。
调整年轻代与老年代的内存占比后,JVM 堆内存分配如下:
根据当前内存占比,我们的 Survivor From 和 Survivor To 区域分别拥有了大概 210 MB 的内存空间,而且 Eden 区域的内存空间也大于了 1000 MB,这就给 Minor GC 带来了很大的操作空间。
上面的的案例分析之所以最终会导致 15 分钟就发生一次 Full GC 的根本原因是,每次发生 Minor GC 时候产生的 200 MB 对象因 Survivor 区域空间不足而直接晋级到老年代了,现在且不说 Eden 区不会没分钟都发生 Minor GC,就算是发生了 Full GC 产生了 200MB 的对象,这 200MB 的对象也不会直接晋级到 老年代,因此会大大的降低老年代的 Full GC 的频率,如果 Eden、Survivor 区域有足够多的空间,我们可以把对象的晋级的年龄调大一些,甚至理论上来说就不会发生 Full GC 了。
年轻代的空间越大越好吗?
相信懂与不懂的朋友都会回答不是,确实不是年轻代空间越大越好,老祖宗说过物极必反,JVM 也在堆中进行分带设计,进行分代设计必然是有原因的,我们知道 Minor GC 效率高,那 Minor GC 效率为啥高,这里面有一个根本原因就是年轻代的空间也会相对较小(当然也跟年轻代采用的垃圾回收算法等有关系),试想一下,如果年轻代空间足够大,在进行垃圾回收的时候,单单就标记 GC ROOTS 就需要消耗大量时间,最终在进行垃圾回收的时候同样也要消耗大量时间,如此以来跟 Full GC 又有什么区别呢。
总结:本篇从案例入手,分析了系统运行过程中内存占用情况,以及垃圾回收情况的模拟分析,找到了垃圾回收的问题点后,我们针对性的进行年轻代比例适当调整,来减少 Full GC 的发生频率,希望可以帮助到有需要的小伙伴。
如有不正确的地方欢迎各位指出纠正。
标签:Full,Survivor,--,调优,GC,内存,JVM,Minor From: https://blog.csdn.net/weixin_42118323/article/details/143831764