通过jcmd VM.native_memory detail查看malloc调用栈中含Java_前缀或JNI库名的分配记录,若某函数分配次数#持续增长且无对应free,则确认为JNI native内存泄漏。

怎么确认 JNI 调用正在吃掉 native 内存?
直接看 jcmd <pid> VM.native_memory summary</pid> 输出里 Internal 和 Other 区域是否持续增长,尤其当 Thread、Class、Code 都稳定时,这两块还在涨——大概率是 JNI 侧 malloc 了没 free,或者本地对象(如 C++ new 出的结构体)长期被全局指针持有。
更关键的线索藏在 detail 模式下:jcmd <pid> VM.native_memory detail</pid> 的输出中搜索 malloc 行,重点关注调用栈里含 Java_ 前缀或你 JNI 库名(比如 libmycrypto.so)的条目。如果某类分配反复出现在同一函数、且 size/次数随请求量线性上升,基本就是泄漏点。
为什么必须用 detail 模式,而不是 summary?
summary 只告诉你 “Internal 用了 400MB”,但完全不知道这 400MB 是谁干的;detail 才会记录每次 malloc 的调用栈、大小、内存类型(mtInternal 或自定义 mtJNIMisc),这是定位 JNI 泄漏唯一可行的路径。
- 启用前必须加
-XX:+UnlockDiagnosticVMOptions,否则detail模式不生效 - 不要在高负载生产环境长期开
detail:它会让 malloc/free 调用多一层 JVM 注册开销,吞吐量下降 5–10% - 建议在复现环境(如压测机)启动时就加参数:
-XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions
如何从 NMT detail 输出里揪出具体 JNI 函数?
执行 jcmd <pid> VM.native_memory detail | grep -A 5 -B 5 "libmyjni\.so\|Java_com_example"</pid>(把 libmyjni.so 替换成你自己的库名),你会看到类似:
0x00007f8a1c000000 65536 Internal (reserved=65536, committed=65536)
(malloc=65536 #1234)
(mmap: reserved=0, committed=0)
0x00007f8a2d456789 libmyjni.so + 0xabcde
0x00007f8a2d451234 Java_com_example_NativeProcessor_processData
重点看三行:malloc=65536 是单次分配大小,#1234 是累计分配次数,最后一行调用栈指向你的 JNI 方法。如果这个 #1234 在多次 diff 后只增不减,且没对应 free 记录,问题就锁定了。
注意:Internal 类型的分配默认不带完整 Java 调用栈,所以必须依赖你 JNI 函数名出现在符号地址映射里——确保编译 so 时没 strip 符号(即避免用 arm-linux-androideabi-strip 过度清理)。
光靠 NMT 不够,还得交叉验证什么?
NMT 能告诉你“哪块 native 内存涨了”,但不能 100% 确认是 JNI 代码 bug 还是第三方 so 自身缺陷。必须补两步:
- 用
pmap -x <pid> | grep "libmyjni\|anon"</pid>查看该 so 占用的匿名映射页是否同步增长(排除 mmap 分配干扰) - 在 JNI 函数入口加日志,打印分配前后的
mallinfo().uordblks或(Linux 下)/proc/<pid>/status</pid>中的VmRSS,确认增长节奏是否与业务请求严格匹配 - 如果 so 来自 SDK 或闭源库,尝试用
LD_PRELOAD拦截其 malloc/free(仅限 Linux),强制打点——这比改 JNI 代码更快定位责任方
真正难的不是发现泄漏,而是确认释放逻辑是否被异常路径绕过(比如 JNI 函数里抛了 std::bad_alloc 但没 catch,导致后续 free 被跳过)。NMT 的 detail 输出里找不到对应 free,往往意味着那段释放代码根本没走到。

















