【问题排查系列】JDK1.8 下内存不断增长排查及解决

952 阅读4分钟

概述

通过对搜索集群中的内存不断增长问题的排查,总结排查内存方面的方法和经验。以便记录和参考。

问题表现

  • 发布之后机器内存不断上涨。需要重启才能得到解决。

解决过程

jmap 排查堆内存阶段

  • 使用传统的方式dump内存,然后使用Jprofiler或mat进行分析。
  • 这里使用了对照的方式,即在重启后立即dump堆和运行一天后dump进行快照比对,发现差异较小,且使用了dump:live 和非live两种方式,均未发现堆内有明显异常。
  • 详细操作方式这里不在赘述。

使用NMT排查堆外内存(native memory)

  • 需要添加参数 -XX:NativeMemoryTracking=detail 据说添加了之后性能会下降 5% ~ 10% ,我添加了,没下降。不过还是建议线上慎重,添加一台机器排查问题即可。
  • 设置NMT 的基线: jcmd <pid> VM.native_memory baseline 设置基线之后即可标记一个基准内存状态,过一段时间之后可以比较内存的变化,哪部分增长的最多。
  • 过一段时间后,使用 jcmd <pid> VM.native_memory detail.diff scale=MB 查看内存的变化。
  • 经过一段时间之后变化如下:

Native Memory Tracking:

Total: reserved=15048MB +73MB, committed=13993MB +74MB

-                 Java Heap (reserved=10240MB, committed=10240MB)
                            (mmap: reserved=10240MB, committed=10240MB)

-                     Class (reserved=1224MB, committed=223MB)
                            (classes #30779 +1)
                            (malloc=6MB #100242 +153)
                            (mmap: reserved=1218MB, committed=218MB)

-                    Thread (reserved=1457MB +5MB, committed=1457MB +5MB)
                            (thread #1444 +4)
                            (stack: reserved=1449MB +5MB, committed=1449MB +5MB)
                            (malloc=5MB #7227 +20)
                            (arena=3MB #2887 +8)

-                      Code (reserved=286MB, committed=251MB)
                            (malloc=42MB #41476 +94)
                            (mmap: reserved=244MB, committed=209MB)

-                        GC (reserved=520MB +16MB, committed=520MB +16MB)
                            (malloc=108MB +16MB #153709 +200)
                            (mmap: reserved=412MB, committed=412MB)

-                  Compiler (reserved=5MB, committed=5MB)
                            (malloc=5MB #5673 +3)

-                  Internal (reserved=610MB +3MB, committed=610MB +3MB)
                            (malloc=609MB +3MB #168363 +191)

-                    Symbol (reserved=672MB +50MB, committed=672MB +50MB)
                            (malloc=667MB +50MB #465680 +6396)
                            (arena=5MB #1)

-    Native Memory Tracking (reserved=15MB, committed=15MB)
                            (malloc=1MB #9771 +3390)
                            (tracking overhead=15MB)

-                   Unknown (reserved=20MB, committed=0MB)
                            (mmap: reserved=20MB, committed=0MB)

可以看到其中的 Symbol 部分上涨明显,这部分主要是存储String intern等信息。所以可以初步判断是这部分泄露了。

寻找解决方案

  • 这里有个基础知识,即jdk8 对元空间的变化。可以自行google 查看变化。jdk8 之后永久代移除,元空间存放在native memory中。

  • 在搜NMT 内存泄露等关键字时发现,jdk似乎存在bug,会造成本地内存泄露:bugs.openjdk.java.net/browse/JDK-…

  • 可以看出表现基本一致:

image image

  • 可以看到jdk1.8 131版本发现了这个问题,而我们使用的是101版本,所以怀疑也存在这个问题,于是尝试修改jdk版本来解决此问题。
  • 解决方案是升级jdk,于是升级到jdk1.8.0_202。

结果验证

  • 跟上一步骤一样,这次采用了对比的方式,即一台机器使用原始的jdk版本(101版本),另外一台使用202版本。经过2天的运行后,可以看到以下差距:

  • 明显看出进行在申请内存方面的差异,而差异主要来源于Symbol。和jdk中的bug表现基本一致。
  • 也可以设置 -XX:MaxMetaspaceSize 对元空间进行限制,不过没有测试。因为目前内存泄露的主要原因还是bug,而不是过多的产生了大量的元数据或String interned。

整体排查思路

  • 使用jmap 查看内存情况,查看内存分配。
  • dump 堆快照分析堆内情况,排查内存泄露,注意dump会FGC,线上需下线进行。并且后来思考,如果是堆内存泄露其实不太会造成物理上的内存持续增长。因为堆的大小是确定的。
  • 堆内内存确认没有问题之后排查堆外内存,使用NMT进行排查,设置baseline,然后隔段时间进行比对。
  • 定位问题后查找解决方案,尝试解决。
  • 尝试解决后进行控制变量比对,确认问题真的解决。
  • 调整后线上稳定运行48H,确认调整没有带来其他副作用。

参考资料