一、问题是怎么发现的
- 早上首页中心出现了多台机器的mdc内存报警,观察mdc内存曲线,发现内存在缓慢增加且较往常增幅稍大。
- 观察jvm的gc和内存情况,没有fullgc,但是yonggc和内存的曲线比较紊乱,且在凌晨仍younggc频繁。
- 打开线上京麦首页,暂未发现明显异常。
二、问题带来的影响
- yonggc和内存的曲线比较紊乱,younggc较往常更为频繁,会影响首页中心的接口性能,进而影响京麦首页用户体验。
三、排查问题的详细过程
- 观察jvm的gc和内存曲线,往前追溯,发现在前一天18点50分左右曲线出现了异常。
- 首页中心最近并未操作线上发版和配置修改,只有在前一天18点50分上传了一份白名单文件,初步猜测gc异常和上传的文件有关。
- 为了进一步验证猜想,观察预发环境的gc和内存曲线,和线上吻合。关闭预发环境的白名单更新定时任务,预发环境gc和内存曲线恢复正常。
起因
- 前一天18点50分,上传了一份白名单文件到oss,是替换操作,这个白名单文件之前是7W+商家,替换后是11W+商家,文件大小1.1M。
- 白名单更新机制:定时任务每1min下载一次白名单文件,读取文件流获取所有名单,组装集合更新到内存中。
四、如何解决问题
按需更新:
oss下载文件时能获取到更新时间戳。第一次下载时,将该时间戳保存到内存中。后续下载文件时每次都与内存中的时间戳进行比对,如果有更新才会去下载文件流,然后更新内存中的时间戳。否则视为未修改文件,直接关闭文件流下载。
五、问题分析:为什么1.1M的文件定时任务下载会导致gc异常
问题截图
younggc异常开始,内存泄漏?
younggc解决了内存泄漏的问题?
曲线解释
-Xmx5440M
-Xms5440M
-XX:MaxMetaspaceSize=512M
-XX:MetaspaceSize=512M
-XX:MaxDirectMemorySize=1G
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:+ParallelRefProcEnabled
-XX:ParallelGCThreads=4
-XX:CICompilerCount=3
① 内存和gc曲线不规律,会周期性地出现短时间内多次younggc
分析:
G1垃圾回收器什么时候执行younggc?eden区空间不足时执会行younggc,但是从曲线看在堆内存很低的时候仍会进行younggc,说明G1中的eden区大小是动态变化的。
知识点:
G1为了达到承诺的最大gc停顿时间,会动态调整eden区的大小。
注:G1想要降低停顿时间,就会调低eden区大小,下次younggc要处理的垃圾变少了,自然gc停顿时间就变短了。
问题:那么为什么younggc不和以前一样正常gc,而是变得紊乱了呢?继续往下看
② 一直有younggc但是gc后留下的内存整体呈上升状,出现内存泄漏的现象
分析1:
内存泄漏,却并没有出现内存溢出。一般这种情况,会出现fullgc清理掉那部分younggc无法处理的垃圾。但是没有出现fullgc,而是在频繁的younggc中内存曲线又回归低位,似乎是进行了一次特殊的younggc?
知识点1:
特殊的younggc叫mixedgc,更准确地说叫并发标记周期,因为实际情况,这里并没有发生mixedgc,而是发生了并发标记周期。(发生mixedgc前置条件是发生了并发标记周期,发生并发标记周期前置条件是发生了younggc)
分析2:
1.1M的文件下载存储到了内存中,或者说产生了1.1M的对象,然后就出现了gc异常,而且是产生了质变,但是0.7M的对象却没事?那么咱们可以猜测异常和对象的大小有关系,0.7M和1.1M中间的1M是否就是一个阈值呢?
知识点2:
region大小计算:region=max((xmx+xms)/2*2048,1m),同时region大小必须是2的次幂,最终得到首页中心region大小为2M。
G1认为只要大小超过了一个Region容量一半的对象即为大对象,大对象在G1里面单独存放区域是Humongous Region。Humongous Region可以认为是一系列连续的region,一般来说也被当做老年代,是不会被younggc清理的(有实验性质的参数可以让younggc清理Humongous region)。
结论:
① 定时任务产生的1.1M的大对象,无法被younggc清理,所以会呈现内存泄漏的现象。
② 没有发生内存溢出,是因为在并发标记周期过程中,会进行Humongous Region的清理,所以可以看到内存曲线恢复正常。
③ younggc变得紊乱:一方面,无法被younggc清理的Humongous Region被视作老年代,本身就积压了eden区的大小。另一方面,因为1.1M的大对象里面的string对象被引用,所以这部分没有被放到Humongous Region,但是younggc也无法清理,只能是younggc后被复制到S0或S1区,这导致了younggc的时间变长,所以下次younggc时会调整eden区大小来达到gc目标停顿时间的要求。
注:这里1.1M的大对象有多个,每个1.1M的大对象都会占用一个2M的region,也浪费了内存。
六、总结反思:是否可以更快发现问题?如何再次避免等。
① jvm的younggc次数设置不敏感,MDC的内存报警无法直观的体现问题
② java应用的jvm参数设置针对不同类型的应用会有不同的最佳实践,垃圾回收器针对大对象
③ jvm的gc监控目前来看并不能区分G1的younggc和并发标记周期。