排查jni泄漏需分清全局引用与native内存两类:前者致full gc频发,用jfr查jniglobalreference事件;后者耗尽虚拟内存,用nmt detail追踪malloc调用栈;须交叉验证并辅以raii封装等编码规范。

排查JNI全局引用泄露和定位Native内存变量,关键在分清两类泄漏、用对工具、抓准时机。全局引用泄漏影响Java堆,表现为Full GC频发;Native内存泄漏消耗进程虚拟内存,最终触发OOM或被系统杀掉。两者常共存,但诊断路径完全不同。
看JFR找全局引用增长点
全局引用泄漏的核心特征是:引用数只增不减,Java对象无法回收,GC压力持续升高。必须用JFR捕获真实行为:
- 启动时加参数:
-XX:+FlightRecorder -XX:StartFlightRecording=duration=60s,filename=/tmp/jni-ref.jfr,settings=profile -J-XX:+UnlockDiagnosticVMOptions - 重点分析
JNIGlobalReferenceCreated与JNIGlobalReferenceDeleted事件,对比数量差值 - 若某类对象(如
java.lang.Class或自定义业务类)的NewGlobalRef调用栈反复出现,且无对应DeleteGlobalRef,基本锁定泄漏源 - 特别注意Attach到JVM的非Java线程——它们创建的全局引用不会随方法退出自动清理,必须显式配对释放
用NMT detail查Native内存分配源头
Native内存泄漏无法靠堆dump发现,必须依赖JVM原生内存追踪。summary模式只报总量,detail模式才能看到每次malloc是谁干的:
- 启动参数必须含:
-XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions - 执行
jcmd <pid> VM.native_memory detail</pid>后,用grep -A 3 -B 2 "libxxx\.so\|Java_com_.*"过滤你自己的JNI库或方法名 - 关注三处信息:
malloc=SIZE(单次大小)、#N(累计次数)、调用栈中是否含Java_*或你的so符号地址 - 多次采样对比:若某函数的
#N持续上升,且无对应free=记录,就是泄漏点 - 编译so时务必保留符号表,否则调用栈显示为地址偏移,无法定位到具体C++函数
交叉验证避免误判
单看JFR或NMT都可能漏掉关键线索,需结合现象反推:
- 如果
VM.native_memory summary中Internal和Other区域持续上涨,而Thread/Class/Code稳定,大概率是JNI侧malloc未free - 如果Full GC频繁发生,但老年代使用率不高、新生代GC正常,说明不是Java对象堆积,而是全局引用阻塞了回收——此时应优先查JFR而非NMT
- Android平台可配合
adb shell dumpsys meminfo <package></package>观察Native Heap增长趋势,与NMT数据对照
代码层防御要点
工具只能帮定位,根治靠编码规范:
- 全局引用一律用RAII封装,例如
scoped_global_ref类,构造时NewGlobalRef,析构时DeleteGlobalRef - 避免在循环、回调、线程池任务中无条件创建全局引用;如需跨方法持有,明确生命周期边界并文档化
- Native内存分配统一走包装函数(如
jni_malloc),内部记录调用栈和size,便于后续hook或日志追踪 - 局部引用在循环内必须手动
DeleteLocalRef,防止局部引用表溢出导致崩溃










