关于gc:线上FullGC问题排查实践手把手教你排查线上问题-京东云技术团队

1次阅读

共计 3836 个字符,预计需要花费 10 分钟才能阅读完成。

作者:京东科技 韩国凯

一、问题发现与排查

1.1 找到问题起因

问题起因是咱们收到了 jdos 的容器 CPU 告警,CPU 使用率曾经达到 104%

察看该机器日志发现,此时有很多线程在执行跑批工作。失常来说,跑批工作是低 CPU 高内存型,所以此时思考是 FullGC 引起的大量 CPU 占用(之前有相似状况,告知用户后重启利用后解决问题)。

通过泰山查看该机器内存应用状况:

能够看到 CPU 的确使用率偏高,然而内存使用率并不高,只有 62%,属于失常范畴内。

到这里其实就有点蛊惑了,按情理来说此时内存应该曾经打满才对。

前面依据其余指标,例如流量的忽然进入也狐疑过是 jsf 接口被忽然大量调用导致的 cpu 占满,所以内存使用率不高,不过前面都缓缓排除了。其实在这里就有点束手无策了,景象与猜想不符,只有 CPU 增长而没有内存增长,那么什么起因会导致单方面 CPU 增长?而后又朝这个方向排查了半天也都被否定了。

前面忽然意识到,会不会是监控有“问题”?

换句话说应该是咱们看到的监控有问题,这里的监控是机器的监控,而不是 JVM 的监控!

JVM 的应用的 CPU 是在机器上能体现进去的,而 JVM 的堆内存高额应用之后在机器上体现的并不是很显著。

遂去 sgm 查看对应节点的 jvm 相干状况:

能够看到咱们的堆内存老年代的确有过被打满而后又清理后的状况,查看此时的 CPU 应用状况也能够与 GC 工夫对应上。

那么此时能够确定,是 Full GC 引起的问题。

1.2 找到 FULL GC 的起因

咱们首先 dump 出了 gc 前后的堆内存快照,

而后应用 JPofiler 进行内存剖析。(JProfiler 是一款堆内存剖析工具,能够间接连接线上 jvm 实时查看相干信息,也能够剖析 dump 进去的堆内存快照,对某一时刻的堆内存状况进行剖析)

首先将咱们 dump 进去的文件解压,批改后缀名.bin,而后关上即可。(咱们应用行云上自带的 dump 小工具,也能够本人去机器上通过命令手工 dump 文件)

首先抉择 Biggest Objects,查看过后堆内存中最大的几个对象。

从图中能够看出,四个 List 对象就占据了 近 900MB 的内存,而咱们刚刚看到堆内存最大也只有 1.3GB,因而再加上其余的对象,很容易就会把老年代占满引发 full gc 的问题。

抉择其中一个 最大的对象作为咱们要查看的对象

这个时候咱们曾经能够定位到对应的大内存对象对应的地位:

其实至此咱们曾经可能大略定位出问题所在,如果还是不确定的话,能够查看具体的对象信息,办法如下:

能够看到咱们的大 List 对象,其实外部是很多个 Map 对象,而每个 Map 对象中又有很多键值对。

在这里也能够看到 Map 中的相干属性信息。

也能够在以下界面间接看到相干信息:

而后一路点上来就能够看到对应的属性。

至此,咱们实践上曾经找到了大对象在代码中的地位。

二、问题解决

2.1 找到大对象在代码中的地位与问题的根本原因

首先咱们根据上述过程找到对应地位与逻辑

咱们的我的项目中大略逻辑是这样的:

  1. 首先会解析用户上传的 Excel 样本,并将其加载到内存中作为一个 List 变量,即咱们上述看到的变量。一个 20w 的样本,此时字段数量有 a 个,大略占用空间 100mb 左右。
  2. 而后遍历循环用户样本,依据用户样本中的数据,再减少一些额定的申请数据,依据此数据申请相干后果。此时字段数量有 a + n 个,占用空间曾经在 200mb 左右。
  3. 循环实现后将此 200mb 的数据存入缓存。
  4. 开始生成 excel,将 200mb 数据从缓存中取出,并依据之前记录的 a 个字段,取出初始的样本字段填充至 excel。

用流程图示意为:

联合一些具体排查问题的图片:

其中一个景象是每次 gc 后的最小内存正在逐渐变大,对应上述步骤中第二步,内存正在逐渐收缩。

论断

将用户上传的 excel 样本加载到内存中,并将其作为一个 List<Map<String, String>> 的构造存储起来,首先一个 20mb 的 excel 文件以此形式存储会收缩占用 120mb 左右堆内存 ,此步骤会大量占用堆内存, 并且因为工作逻辑起因,该大对象内存会在 jvm 中存在长达 4 -12 小时之久,导致一但工作过多,jvm 堆内存很容易被打满。

这里列举了为什么应用 HashMap 会导致内存收缩,其次要起因是存储空间效率比拟低:

一个 Long 对象占内存计算:在 HashMap<Long,Long> 构造中,只有 Key 和 Value 所寄存的两个长整型数据是无效数据,共 16 字节(2×8 字节)。这两个长整型数据包装成 java.lang.Long 对象之后,就别离具备 8 字节的 MarkWord、8 字节的 Klass 指针,再加 8 字节存储数据的 long 值(一个包装对象占 24 字节)。

而后这 2 个 Long 对象组成 Map.Entry 之后,又多了 16 字节的对象头(8 字节 MarkWord+ 8 字节 Klass 指针 =16 字节),而后一个 8 字节的 next 字段和 4 字节的 int 型的 hash 字段(8 字节 next 指针 + 4 字节 hash 字段 + 4 字节填充 =16 字节),为了对齐,还必须增加 4 字节的空白填充,最初还有 HashMap 中对这个 Entry 的 8 字节的援用,这样减少两个长整型数字,理论消耗的内存为(Long(24byte)×2)+Entry(32byte)+HashMapRef(8byte)=88byte,空间效率为无效数据除以全副内存空间,即 16 字节 /88 字节 =18%。

——《深刻了解 Java 虚拟机》5.2.6

以下是刚上传的 excel 中 dump 出的堆内存对象,其占用的内存达到了 128mb,而上传的 excel 理论只有 17.11mb。

空间效率 17.1mb/128mb≈13.4%

2.2 如何解决此问题

暂且不探讨上述流程是否正当,解决办法个别能够分为两类,一类是治标 ,即不把 该对象放入 jvm 内存中 ,转而存入缓存中,不在内存中则大对象问题天然迎刃而解。 另一类是治本,即放大该大内存对象,在日常应用场景下使其个别不会触发频繁的 full gc 问题。

两种形式各有优劣:

2.2.1 激进医治:不把他存入内存

解决逻辑也很简略,例如在加载数据时,将其依照样本加载数据一条一条存入 redis 缓存,而后咱们只须要晓得样本中有多少的数量,依照数量的先后顺序从缓存中取出数据,即可解决该问题。

长处:能够从根本上解决此问题,当前基本上不会存在该问题,数据量再大只须要增加相应的 redis 资源即可。

毛病:首先会减少许多 redis 缓存空间耗费,其次从显示思考对于咱们我的项目来说,此处代码古老且艰涩难懂,改变须要较大工作量与回归测试。

2.2.2 激进医治:缩减其数据量

剖析 2.1 的上述流程,首先第三步是齐全没必要的,先存入缓存再取出,额定占用缓存空间。(猜想系历史问题,此处不再深究)。

其次是在第二步中,多进去的字段 n,在申请完结后该字段就曾经无用了,因而能够思考在申请完结后删除无用字段。

此时也有两种解决方案,一种是 只删除无用字段缩减其 map 大小 ,而后将其作为参数传递给生成 excel 应用; 另一种形式是申请实现间接删除该 map,而后在生成 excel 时再从新读取用户上传的 excel 样本。

长处:改变较小,不须要太简单的回归测试

毛病:在极其大数据量状况下,仍有可能呈现 full gc 的状况

具体实现形式就不开展了。

其中一种实现形式

// 获取有用的字段
String[] colEnNames = (String[]) colNameMap.get(Constant.BATCH_COL_EN_NAMES);
List<String> colList = Arrays.asList(colEnNames);
// 去除无用的字段
param.keySet().removeIf(key -> !colList.contains(key));

三、拓展思考

首先本文中监控图是在复现过后场景时人为制作的 gc 常见。

在 cpu 使用率图中,大家能够察看到 cpu 使用率上升时间的确跟 gc 的工夫相吻合,然而并没有呈现过后场景中的 104% 的 CPU 使用率

其实间接起因比较简单,就是因为零碎尽管呈现了 full gc,然而并没有 频繁 呈现。

小范畴低频率的 full gc 不太会引起零碎的 cpu 飙升,这也是咱们所看到的景象。

那么过后的场景是什么起因呢?

咱们上文提到过,咱们在 堆内存中的大对象是会随着工作的进行逐渐收缩的 ,那么当咱们的工作足够多,工夫足够长,就有可能导致每次 full gc 后可用空间变得越来越小,当可用空间小到肯定水平之后就, 每次 full gc 实现之后发现空间还是不够应用,就会触发下一次的 gc,从而导致最终后果的频繁产生 gc,引起 cpu 频率的飙升不下。

四、问题排查总结

  • 当咱们遇到线上 cpu 使用率过高的状况时,能够先查看是否是 full gc 引起的问题,留神要看的是 jvm 的监控,或者应用 jstat 相干命令查看。不要被机器内存监控所误导。
  • 如果确定是 gc 引起的问题,能够通过 JProfiler 直连线上 jvm 或者应用 dump 保留堆快照后离线剖析。
  • 首先能够找到最大的对象,个别状况下是大对象引起的 full gc。还有一种状况是,不像这么显著是四个大对象,也可能是比拟平衡的十几个 50mb 的对象,具体情况还须要具体分析。
  • 通过上述工具找到确定有问题的对象后找到其堆栈对应的代码地位,通过代码剖析找到问题的具体起因,通过其余景象推演猜想是否正确,进而找到问题的真正起因。
  • 依据问题的起因解决此问题。

当然,上述只是不算很简单的排查状况,不同的零碎必定有不同的内存状况,咱们该当具体问题具体分析,而从此次问题中能够学到的就是如果排查解决问题的思路。

正文完
 0