排查JNI内存泄漏需区分全局引用泄漏与Native内存未释放:前者致GC频繁,后者使RSS飙升;用jcmd VM.native_memory detail定位malloc点,JFR分析全局引用增减,编译保留符号表并规范RAII封装。

排查 JNI 本地内存泄漏,核心是区分“JNI 全局引用泄漏”和“Native 内存分配未释放”两类问题——前者拖慢 GC、后者直接吃掉进程 RSS。不能只看堆,得用对工具、抓准线索、交叉验证。
先确认是不是 JNI 引起的 native 内存涨
执行 jcmd <pid> VM.native_memory summary,重点看三处:
- Internal 和 Other 区域持续上涨,而 Thread / Class / Code / Direct 等模块稳定 → 很可能是 JNI 侧 malloc 未 free
- 如果 RSS(top 中 RES)涨到 8GB+,但 jstat -gc 显示老年代使用率始终低于 30%、Full GC 极少 → 堆正常,问题在堆外
- 进程被 Linux OOM Killer 杀掉,/var/log/messages 里有 “Out of memory: Kill process … (java)” 且无 Java OOM 日志 → 典型 native 内存耗尽信号
用 NMT detail 锁定具体 C/C++ 函数
NMT summary 只报总量,必须开 detail 模式才能看到谁 malloc 了、在哪调的、调了多少次:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 启动参数加:-XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions(注意顺序,必须在 java 命令最前)
- 运行中执行:jcmd <pid> VM.native_memory detail | grep -A 5 -B 5 "libxxx\.so\|Java_com_"(把 libxxx.so 替换为你自己的 JNI 库名)
- 关注输出里的三行关键信息:malloc=SIZE(单次大小)、#N(累计次数)、调用栈中是否含你的 JNI 方法(如 Java_com_example_NativeProcessor_processData)
- 多次采样对比:若某函数的 #N 持续上升,且没看到对应 free= 记录 → 泄漏点基本坐实
同步查 JNI 全局引用是否堆积
全局引用泄漏不会让 RSS 暴涨,但会让对象无法回收,引发频繁 Full GC:
立即学习“Java免费学习笔记(深入)”;
- 启动时加:-XX:+FlightRecorder -XX:StartFlightRecording=duration=120s,filename=/tmp/jni.jfr,settings=profile
- 用 JDK 自带的 JDK Mission Control 打开 jni.jfr,筛选 JNIGlobalReferenceCreated / Deleted 事件
- 重点关注差值大的类:比如 NewGlobalRef 调用 1000 次,DeleteGlobalRef 只有 200 次,且调用栈集中在某个 attach 线程或循环逻辑里
- 特别注意非 Java 线程(如 C++ 主动 Attach 的线程)创建的全局引用——它们不会随方法退出自动清理,必须手动配对 delete
代码和编译层面要配合验证
工具只能提示位置,修复靠规范:
- JNI so 文件编译时务必保留符号表:不要 strip -s 或过度清理,否则 NMT 调用栈只显示地址偏移,无法定位到函数名
- 全局引用统一用 RAII 封装(如 C++ 的 scoped_global_ref),避免裸调 New/Delete
- 所有 malloc/new 出的 native 对象,必须有明确的释放路径;尤其注意异常分支、early return 场景是否遗漏 free/delete
- 第三方 native 库(如 OpenCV Mat、SQLite statement、Netty epoll channel)要严格遵循其 close/release 文档,不依赖 JVM GC

















