关于 堆外内存的组成可以看上一篇文章 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笔记,可以更及时回复你的讨论。