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 检测内存泄露
- 开启 NMT,用命令:
-XX:NativeMemoryTracking=summary|detail
- 创建基线,用命令:
jcmd <pid> VM.native_memory baseline
- 观察内存变化:
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=summary
与 jcmd <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=detail
与 jcmd <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.bin
或 strings /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笔记,可以更及时回复你的讨论。