记一次 CMS 回收异常问题 —— 跨代引用和循环依赖

梦想的初衷 提交于 2021-01-02 15:23:54

模型系统加载深度学习模型后,会触发报警,原因是触发了 Full GC,而 GC 后回收效率却不高,回收前 83%,回收后 65%,老年代加载完成单率模型后,竟然从 150M 飙到了 1.5G 以上,而实际上用 jol 计算出来的模型大小才 300M,这明显是不符合预期的。最后排查发现实际是 跨代引用和循环依赖 导致的问题。

GC 日志

GC日志 整理如下:

服务启动
1 YGC,from survivor 占用 64% - 0.59s
2 YGC,from survivor 占用 24%,有 165823K 进入老年代,被回收的不多,差不多 10M - 0.43s
3 YGC,from survivor 占用 10%,老年代使用了 165823K,结果没变,但是发现 eden 区没有用满就触发 Young GC 了,Eden 区用了 47% - 0.03s
【这个过程是在 Full GC 之前,可以看到触发条件是 System.gc()】
  - 初始标记 0.01s STW
  - 并发标记 0.03s
  - 并发预清理 0.01s + 5.4s
  - 最后标记 0.11s STW
  - 并发清理 0.15s
  - 并发重启 0.04s
  也就是说这次 Full GC 有 0.12s 会 Stop the world,而最后老年代还剩余 165820K,回收了 3K
4 YGC,from survivor 占用 21%,老年代使用了 165820K - 0.14s
5 YGC,from survivor 占用 44%,老年代使用了 165820K - 0.23s
6 YGC,from survivor 占用 64%,老年代使用了 165820K - 0.36s
7 YGC,from survivor 占用 88%,老年代使用了 254776K - 0.52s
8 YGC,from survivor 占用 71%,老年代使用了 339702K - 0.44s
9 YGC,from survivor 占用 56%,老年代使用了 424914K - 0.29s
10 YGC,from survivor 占用 88%,老年代使用了 424914K - 0.22s
11 YGC,from survivor 占用 92%,老年代使用了 528118K - 0.43s
12 YGC,from survivor 占用 87%,老年代使用了 528118K - 0.26s
13 YGC,from survivor 占用 100%,老年代使用了 602525K - 0.41s
14 YGC,from survivor 占用 78%,老年代使用了 675399K - 0.38s
15 YGC,from survivor 占用 74%,老年代使用了 741619K - 0.30s
16 YGC,from survivor 占用 100%,老年代使用了 741693K - 0.34s
17 YGC,from survivor 占用 87%,老年代使用了 814652K - 0.44s

...

29 YGC,from survivor 占用 81%,老年代使用 1975662K - 0.76s
30 YGC,from survivor 占用 72%,老年代使用 2181070K - 1.22s (2621440K)
【触发 Full GC,这次的触发条件是 Old 区比例过大,达到 83%,超过了配置的 80%】
  - 初始标记 0.09s STW
  - 并发标记 2.76s
  - 并发预清理 0.36s + 5.67s

31 期间触发了一次 YGC,survivor 占用 79%, 老年代 2181070K - 0.44s

  - 最后标记 0.61s STW 2181070K(2621440K) 3564186K(4806016K)
  - 并发清理 2.65s

32 期间触发了一次 YGC,survivor 占用 100%,老年代 1721146K - 0.64s(降到了 65.6%)

  - 并发重启 0.01s
  
接下来 GC 后老年代 1815643K 又继续涨,直到模型加载完成,而老年代基本稳定在 1907123K

执行 jmap dump:live 后触发 Full GC,回收正常,老年代很多对象被回收了

问题排查

  • 加载模型成功后,计算模型大小,用到了 openjdk.jol
  • 计算模型大小的过程很长,触发了很多次 GC,而计算完后会显式调用 System.gc()
  • 这次调用后,发现老年代基本没有变小,还有将近 2g 没有被回收
  • jmap -dump:format=b,file=XX PID dump 了堆文件,用 VisualVM 分析,发现有很多是 jol 的对象
  • 不去计算对象大小,就不会有这个问题,所以可以判定是 jol 引起的
  • 在 dump 的时候,加上了 live 选项,发现触发了 CMS Full GC,日志和之前完全不同,这点比较困惑
  • 后来网上查到资料,说 如果添加了并发 CMS 的配置,不会触发 Full GC,而是 CMS GC,也就是 Old GC,但是 jstat 会看到 full gc time 增加2次,因为 stop the world 2次
  • 删掉参数,触发果然能 Full GC,清理掉了所有垃圾,因此怀疑是单清除 Young 区 和 Old 区不会有效果
  • 具体原因猜测为 Young 区和 Old 区出现了循环引用,导致单回收 Young 区无法回收部分对象,单回收 Old 区也无法回收那些对象。为什么会这样,因为多数 对象要么同时在 Young 区,要么同时在 Old 区,而计算模型比较特殊,是一棵树的结构,导致上层树节点对象先晋升到了 Old 区,而引用他们底层树节点对象的在 Young 区,因而无法做到回收
  • 回头查看了一下 GC 日志,发现每次 young gc 果然有 600多M 没有被回收
  • 做了一个实验,调小年代晋升的阈值=5,连续触发 System.gc():触发 young gc 和 cms gc,可以发现一开始 young 区的 age 在不断增加,随后这些对象进入 老年, 5次 之后,young 区的数据开始变少,连续触发多次,old 区垃圾完全被清掉
  • 原因在于 young 区的内容随着年代生长,进入到了 old 区,cms 回收就可以成功回收掉 old 区了,前面猜想得到证实
  • 解决方案:1. 不用并发方式,这种不可取,线上服务性能牺牲大;2. 另起一个服务专门做这个大小计算;

总结

问题排查的过程中,发现自己对 CMS 垃圾回收器和各类 GC 的区别理解的并不够深入,导致排查过程前期花费了不必要的时间,假使知道 CMS GC 和 Full GC 的区别,没有混淆两者的概念,应该会很快猜到引发问题的原因。好在最终解决了这个问题,也更深入了解了 CMS 垃圾回收(还远远不够)~ 这种问题倒是不常遇到

参考资料

https://blog.csdn.net/scugxl/article/details/50935863 讲了触发 Full GC 的情况

https://www.zhihu.com/question/264164591/answer/279604329 从这里发现了问题,禁掉并发是会触发 Full GC 的

https://www.jianshu.com/p/5ace2a0cafa4 这里面讲了各种 GC 的区别,可以发现使用 CMS,如果添加了并发CMS GC的话,System.gc() 是不会触发 full gc 的

http://lovestblog.cn/blog/2015/05/07/system-gc/ 你假笨大神的文章,这里面讲述了 CMS GC 有两种模式,foreground 和 background,上面的是 background,这种 GC 不是 full GC,而是老年代的回收

https://www.jianshu.com/p/5037459097ee 跨带引用

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!