如何进行一次年轻代GC长暂停问题的解决与思考,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。公司某规则引擎系统,在每次发版启动会手动预热,预热完成当流量切进来之后会偶发的出现一次长达1-2秒的年轻代GC(流量并不大,并且LB下的每一台服务都会出现该情况)在这次长暂停之后,每一次的年轻代GC暂停时间又都恢复在20-100ms以内2s虽然看起来不长,但是对比规则引擎每次10ms左右的响应时间来说,还是不可以接受的;并且由于该规则引擎响应超时,还会导致出单超时失败在分析该系统GC日志后发现,2s暂停发生在Young GC阶段,而且每次发生长暂停的Young GC都会伴随着新生代对象的晋升(Promotion)核心JVM参数(Oracle JDK7)启动后第一次年轻代GC日志长暂停年轻代GC日志从这个长暂停的GC日志来看,是发生了晋升的,在Young GC后,有363M+的对象晋升到了老年代,这个晋升操作因该就是耗时原因(ps: 检查过safepoint原因,不存在异常)由于日志参数中没有配置-XX:+PrintHeapAtGC
参数,这里是手动计算的晋升大小:年轻代年轻变化 – 全堆容量变化 = 晋升大小
(304371K – 3730075K) – (676858K – 3730075K) = 372487K(363M)下一次年轻代GC日志乍一看其实没什么问题,仔细想想发现了一些不正常,为什么程序刚启动第二次gc就发生了晋升呢这里应该是动态年龄判定导致的,GC中晋升年龄阈值并不是固定的15,而是jvm每次gc后动态计算的为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄
《深入理解Java虚拟机》一书中提到,对象晋升年龄的阈值是动态判定的。不过经查阅其他资料和验证后,发现此处和《深入理解Java虚拟机》解释的有些出入(或者是书上解释的不够清楚)其实就是按年龄给对象分组,取total(累加值,小于等与当前年龄的对象总大小)最大的年龄分组,如果该分组的total大于survivor的一半,就将晋升年龄阈值更新为该分组的年龄注意:不是是超过survivor一半就晋升,超过survivor一半只会重新设置晋升阈值(threshold),在下一次GC才会使用该新阈值从上面第一次的GC日志也可以证明这个结论,在这次GC中全堆的内存变化和年轻代内存变化是相等的,所以并没有发生对象的晋升就像上面的日志中,第一次GC只是将threshold设置为1,因为此时survivor一半为214728704 bytes,而年龄为1的对象总和有315529928 bytes,超过了Desired survivor size,所以在本次GC后将threshold设置为年龄为1的对象年龄1这里更新了对象晋升年龄阈值为1这里顺便解释下这个年龄分布的输出内容:age 1表示年龄为1的对象分组,315529928 bytes
表示年龄为1的对象占用内存大小315529928 total
这个是一个累加值,表示小于等于当前分组年龄的对象总大小。先把对象按年龄分组,age 1的分组total为age 1总大小(前面的xxx bytes),age 2的分组total为age 1 + age 2
总大小,age n的分组total为age 1 + age 2 + ... +age n
的总大小,累加规则如下图所示当total最大的分组的total值超过了survivor/2时,就会更新晋升阈值在第二次年轻代GC“长暂停年轻代GC日志”中,由于新的晋升年龄阈值为1,所以那些经历了一次GC并存活并且现在仍然可达(reachable)的对象们就会发生晋升了由于此次GC发生了363M的对象晋升,所以导致了长暂停JVM中这个“动态对象年龄判定”真的是合理的吗?个人认为机制是好的,可以更好的适应不同程序的内存状况,但不是任何场景都适合,比如在本文中这个刚启动不就GC的场景下就会有问题因为在程序刚启动时,大多数对象年龄都是0或者1,很容易出现年龄为1的大量存活对象;在这个“动态对象年龄判定”机制下,就会导致新的晋升阈值被设置为1,导致这些不该晋升的对象发生了晋升比如程序在初始化,正在加载各种资源时发生了Young GC,加载逻辑还在执行中,很多新建的对象年龄在这次GC时还是可达的(reachable)经历了这次GC后,这些对象年龄更新为1,但是由于“动态对象年龄判定”机制的影响,晋升年龄阈值更新为了“最大的对象年龄分组”的年龄,也就是这批刚经历了一次GC的对象们在这次GC之后不久,资源初始化完成了,涉及的相关对象有很可能不可达了,但是由于刚才晋升年龄阈值被更新为了1,在下一次正常的Young GC这批年龄为1的对象会直接发生晋升,提前或者说错误的发生了晋升经查阅文档、资料,发现“动态年龄判定”这个机制并不能禁用,所以如果想解决这个问题,只有靠“绕过”这个计算规则了动态年龄的判定,是根据Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半来判定的,那么根据这个机制解决也很简单由于我们足够了解自己的系统,清楚的知道加载资源所需的大概内存,完全可以设定一个大于这些暂时可达的对象总和的数值来作为survivor的容量比如上面的日志中,第一次GC后年龄为1的对象有315529928 Bytes(300M),Desired survivor size为(survivor size /2)214728704 bytes(204M),那么survivor就可以设置为600M以上。不过为了稳妥,还是将survivor调到800M,这样desired survivor size就是400M左右,在第一次Young GC后,就不会因年龄为1的对象总和超过了desired survivor size而导致晋升年龄阈值的更新了,从而也就不会有提前/错误晋升而导致的GC长暂停问题survivor不可以直接指定大小,不过可以通过-XX:SurvivorRatio这种调节比例的方式来调节survivor大小-XX:SurvivorRatio=8
表示两个Survivor和Edgen区的比,8表示两个Survivor:Eden=2:8,即一个Survivor占新生代的1/10。计算方式为:为什么晋升300M比年轻代回收3G还要慢这么多倍
根据复制算法的特性,复制算法的时间消耗主要取决于存活对象的大小,而不是总空间的大小比如上面4G的年轻代(实际只有Eden+S0可用),GC时只需要从GC ROOTS开始遍历对象图,将可达的对象复制至S1即可,并不需要遍历整个年轻代在上面那次长暂停GC日志中,发生了363M的晋升,300M左右的回收,对比第一次GC基本可以得出,花费的1.5S基本上都是在晋升操作那么为什么晋升操作这么耗时呢?这里没有深入研究Oracle JVM实现的年轻代晋升细节,不过晋升涉及跨代复制(其实都年轻代和老年代都是heap,在复制这件事上本质上没什么区别,都是memcpy而已,只是需要额外处理的逻辑更多了)
,所需处理的逻辑会更复杂一些,比如指针的更新等操作,更耗时也是可以理解的,这里也附上一段可以在本地模拟问题的代码,Oracle JDK7下可直接运行测试jvm options-server -Xmn400M -XX:SurvivorRatio=9 -Xms1000M -Xmx1000M -XX:+PrintGCDetails -XX: 香港云主机+PrintGCDateStamps -XX:+PrintTenuringDistribution -XX:+PrintHeapAtGC -XX:+PrintReferenceGC -XX:+PrintGCApplicationStoppedTime -XX:+UseConcMarkSweepGC看完上述内容,你们掌握如何进行一次年轻代GC长暂停问题的解决与思考的方法了吗?如果还想学到更多技能或想了解更多相关内容,欢迎关注开发云行业资讯频道,感谢各位的阅读!
本篇文章为大家展示了 windows中怎么安装 MySQLdb ,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。一、环境(windows10 + python3.8)二、安装(失败)1、通过命令行安装:pip insta…
免责声明:本站发布的图片视频文字,以转载和分享为主,文章观点不代表本站立场,本站不承担相关法律责任;如果涉及侵权请联系邮箱:360163164@qq.com举报,并提供相关证据,经查实将立刻删除涉嫌侵权内容。