模型系统加载深度学习模型后,会触发报警,原因是触发了 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 跨带引用
来源:oschina
链接:https://my.oschina.net/u/4362986/blog/3852242