Java 应用在 Linux 上堆外内存泄漏怎么使用 NMT 工具排查?

文章导读
如果 JVM 启动时未开启 NativeMemoryTracking 参数,NMT 无法事后启用,此时应优先通过 pmap 或系统监控定位内存占用大的 native 库,若已开启则直接使用 jcmd 导出内存快照对比。
📋 目录
  1. 命令速用版
  2. 原理简述
  3. 分步处理
  4. 怎么验证是否生效
  5. 常见坑
  6. 参考来源
A A

如果 JVM 启动时未开启 NativeMemoryTracking 参数,NMT 无法事后启用,此时应优先通过 pmap 或系统监控定位内存占用大的 native 库,若已开启则直接使用 jcmd 导出内存快照对比。

先说结论:NMT 是 JDK 自带的原生内存跟踪工具,但必须在 Java 进程启动时添加参数才能生效,无法用于排查未开启该参数的现存进程。

  • 先确认:检查目标进程启动参数是否包含 -XX:NativeMemoryTracking=summary 或 detail。
  • 先处理:若未开启,需重启应用并添加参数;若已开启,使用 jcmd 命令导出内存快照。
  • 再验证:对比两次快照的 diff 结果,确认增长区域是否对应代码中的堆外分配逻辑。

命令速用版

以下命令需在 JDK bin 目录下执行,且执行用户需与 Java 进程所有者一致。

# 1. 检查 NMT 是否已启用
jcmd <pid> VM.native_memory summary

# 2. 设定基线(应用稳定后执行)
jcmd <pid> VM.native_memory baseline

# 3. 问题复现后导出差异快照
jcmd <pid> VM.native_memory diff > diff.txt

原理简述

Java 堆内存(Heap)由 JVM 统一管理,但堆外内存(Off-Heap)包含直接缓冲区、元空间、线程栈、代码缓存以及 JNI 调用分配的 native 内存。JVM 进程总占用(RSS)往往大于堆内存设置值,多出的部分即为原生内存。NMT 通过拦截 JVM 内部的 native 内存分配调用(如 malloc/mmap)来记录分类使用情况,但它不是系统级探针,必须 JVM 主动配合记录。

分步处理

步骤 1:确认 NMT 状态

执行 jcmd <pid> VM.native_memory summary。如果返回 Native Memory Tracking is not enabled,说明当前进程无法使用 NMT 排查。

步骤 2:开启 NMT(需重启)

在 Java 启动脚本中添加参数:-XX:NativeMemoryTracking=summary。若需查看调用栈定位代码位置,使用 detail 模式,但需注意 detail 模式会带来更高的性能开销和内存占用。

步骤 3:设定基线快照

应用启动稳定后,立即执行一次 jcmd <pid> VM.native_memory baseline。此命令会将当前内存状态标记为基准,供后续 diff 命令对比。

步骤 4:采集问题快照并对比

Java 应用在 Linux 上堆外内存泄漏怎么使用 NMT 工具排查?

当发现内存占用升高时,执行 jcmd <pid> VM.native_memory diff > diff.txt。diff 命令会对比之前通过 baseline 命令设定的基准快照,显示增加或减少的内存区域。

步骤 5:分析增长项

查看 diff 文件中增长明显的类别。常见泄漏点包括 Java Heap 之外的 Direct BufferThread(线程过多)、Code CacheInternal(通常涉及 JNI 或 GC 结构)。

怎么验证是否生效

1. 命令反馈:执行 jcmd 命令后能正常输出内存分类表格,且包含 reservedcommitted 数值,即表示 NMT 工作正常。

2. 日志观察:若开启 detail 模式,JVM 启动日志中会出现 Native Memory Tracking: On (level: detail) 字样。

3. 趋势吻合:NMT 报告的总 native 内存增长趋势应与 Linux 系统监控工具(如 top 或 ps 看到的 RSS)增长趋势基本一致。若 NMT 显示不变但 RSS 持续上涨,可能存在 JVM 未纳管的 native 泄漏(如第三方 native 库直接 malloc)。

常见坑

1. 事后无法开启:NMT 不是 Agent,不能通过 attach 方式动态开启,必须重启应用。

2. 权限问题:jcmd 命令通常要求执行用户与 Java 进程所有者相同,否则可能报错 Connection refused 或看不到进程。

3. 开销控制:官方文档指出 detail 模式开销显著高于 summary 模式,生产环境建议优先使用 summary,仅在复现环境使用 detail 定位调用栈。

4. 非 JVM 内存:NMT 只能跟踪 JVM 自身管理的 native 内存。如果应用加载了第三方 C/C++ 库且该库自行分配内存未通过 JVM 接口,NMT 无法统计这部分泄漏。

参考来源

  • Oracle Official Documentation, Java SE Documentation: Native Memory Tracking, URL: https://docs.oracle.com/en/java/javase/11/vm/native-memory-tracking.html
  • Oracle Official Documentation, Java SE Documentation: jcmd tool, URL: https://docs.oracle.com/en/java/javase/11/tools/jcmd.html