JVM 堆外内存泄漏分析(二)

关于 堆外内存的组成可以看上一篇文章 JVM 堆外内存泄漏分析(一)

1. NMT

NMT(Native Memory Tracking)是 HotSpot JVM 引入的跟踪 JVM 内部使用的本地内存的一个特性,可以通过 jcmd 工具访问 NMT 数据。NMT 目前不支持跟踪第三方本地代码的内存分配和 JDK 类库。

NMT 不跟踪非 JVM 代码的内存分配,本地代码里的内存泄露需要使用操作系统支持的工具来定位。

1.1 开启 NMT

启用 NMT 会带来 5-10% 的性能损失。NMT 的内存使用率情况需要添加两个机器字 word 到 malloc 内存的 malloc 头里。NMT 内存使用率也被 NMT 跟踪。

启动命令: -XX:NativeMemoryTracking=[off | summary | detail]

off:NMT 默认是关闭的;
summary:只收集子系统的内存使用的总计数据;
detail:收集每个调用点的内存使用数据。

1.2 jcmd 访问 NMT 数据

命令: jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]

option desc
summary 按分类打印汇总数据
detail 按分类打印汇总数据
打印虚拟内存映射
按调用点打印内存使用汇总
baseling 创建内存使用快照用于后续对比
summary.diff 基于最新的基线打印一份汇总报告
detail.diff 基于最新的基线打印一份明细报告
shutdown 关闭 NMT

在 NMT 启用的情况下,可以通过下面的命令行选项在 JVM 退出时输出最后的内存使用数据:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics

1.3 使用 NMT 检测内存泄露

  1. 开启 NMT,用命令: -XX:NativeMemoryTracking=summary|detail
  2. 创建基线,用命令: jcmd <pid> VM.native_memory baseline
  3. 观察内存变化: jcmd <pid> VM.native_memory detail.diff

NMT 数据输出解释:

reserved memory:预订内存,不表示实际使用,最主要的是申请了一批连续的地址空间;(OS 角度)
commited memory:实际使用的。(OS 角度)
对于 64 位的系统,地址空间几乎是无限的,但越来越多的内存 committed,可能会导致 swapping 或本地 OOM 。

以下示例来自 https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html

-XX:NativeMemoryTracking=summaryjcmd <pid> VM.native_memory summary 输出:

Total:  reserved=664192KB,  committed=253120KB  <--- total memory tracked by Native Memory Tracking

-     Java Heap (reserved=516096KB, committed=204800KB)  <--- Java Heap
                (mmap: reserved=516096KB, committed=204800KB)

-     Class (reserved=6568KB, committed=4140KB)     <--- class metadata
            (classes #665)                          <--- number of loaded classes
            (malloc=424KB, #1000)                   <--- malloc'd memory, #number of malloc
            (mmap: reserved=6144KB, committed=3716KB)

-     Thread (reserved=6868KB, committed=6868KB)
            (thread #15)                            <--- number of threads
            (stack: reserved=6780KB, committed=6780KB) <--- memory used by thread stacks
            (malloc=27KB, #66)
            (arena=61KB, #30)                       <--- resource and handle areas

-     Code (reserved=102414KB, committed=6314KB)
           (malloc=2574KB, #74316)
           (mmap: reserved=99840KB, committed=3740KB)

-     GC (reserved=26154KB, committed=24938KB)
           (malloc=486KB, #110)
           (mmap: reserved=25668KB, committed=24452KB)

-     Compiler (reserved=106KB, committed=106KB)
               (malloc=7KB, #90)
               (arena=99KB, #3)

-     Internal (reserved=586KB, committed=554KB)
               (malloc=554KB, #1677)
               (mmap: reserved=32KB, committed=0KB)

-     Symbol (reserved=906KB, committed=906KB)
             (malloc=514KB, #2736)
             (arena=392KB, #1)

-     Memory Tracking (reserved=3184KB, committed=3184KB)
                      (malloc=3184KB, #300)

-     Pooled Free Chunks (reserved=1276KB, committed=1276KB)
                         (malloc=1276KB)

-     Unknown (reserved=33KB, committed=33KB)
              (arena=33KB, #1)

-XX:NativeMemoryTracking=detailjcmd <pid> VM.native_memory detail 组合的输出示例:

2. 系统层面的分析思路

内存泄漏一般都不是突然猛增到极限,而是一个慢慢增长的过程,这样我们可以选取两个时间的内存来进行对比,看新增的内存里到底存的是什么内容。

2.0 gdb 方式

gdb 导出指定地址范围的内存块的内容 :

sudo gdb --batch --pid 2754 -ex "dump memory a.dump 0x7f1023ff6000 0x7f1023ff6000+268435456"

然后用 hexdump -C /tmp/memory.binstrings /tmp/memory.bin |less 查看内存块里的内容。

如果内存块里存的是文本信息,这样是可以看出存的是什么内容的,如果是二进制的内存,就没法看了。

2.1 jstack/jmap + core dump

先生成 core dump,然后从 core dump 里提取线程栈、JVM 堆 dump,JDK 8 下提取成功:

# 使用 gcore 命令生成 core dump,
gcore 1791

# 使用 jstack 从 core dump 文件提取线程信息
~/zuul-jdk/zulu8.40.0.25-ca-jdk8.0.222-linux_x64/bin/jstack ~/zuul-jdk/zulu8.40.0.25-ca-jdk8.0.222-linux_x64/bin/java core.1791

# 使用 jmap 从 core dump 文件提取 JVM 堆 dump
~/zuul-jdk/zulu8.40.0.25-ca-jdk8.0.222-linux_x64/bin/jmap -dump:format=b,file=zuul.jmap.hprof ~/zuul-jdk/zulu8.40.0.25-ca-jdk8.0.222-linux_x64/bin/java core.1791

# jstack、jmap 从 core dump 里提取信息的方式,exec 一般是指向可执行命令 java 的路径
jstack exec core-file
jmap <options> exec core-file

2.2 jhsdb

jhsdb: hsdb 是 HotSpot debugger 的简称,是 JDK9 开始引入的一个调试工具。

$ jhsdb
    clhsdb              command line debugger
    hsdb                ui debugger
    debugd --help       to get more information
    jstack --help       to get more information
    jmap   --help       to get more information
    jinfo  --help       to get more information
    jsnap  --help       to get more information

在 openJDK 11 提取实操失败了,生成堆 dump 时会出现一些内存地址读取失败。

用 jstack 从 core dump 提取信息:

sudo jstack -J-d64 /usr/bin/java core.2316

jhsdb jstack --exe /usr/bin/java --core core.2316

-d64 表示64位的系统,这两个也是网上找的,没有实际成功。

3. 参考资料


欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据