堆外内存持续增长,JVM 堆正常,如何定位是 Netty 还是 JNI 泄漏
解读
面试官抛出此题,核心想验证三件事:
- 你是否能把“堆外内存”与“JVM 堆”彻底分离,意识到它不受 GC 直接管控;
- 面对国内生产环境普遍使用的 Netty 框架(网关、RPC、消息中间件),你是否熟悉其 ByteBuf 的引用计数与池化机制;
- 当怀疑 JNI 时,你是否具备从操作系统层面到 JVM 工具链的一整套“证据链”定位思路,而不是拍脑袋猜。
回答时必须给出“可落地的步骤 + 量化指标 + 区分逻辑”,让面试官听到就能复现。
知识点
- 堆外内存(Direct Memory)上限:-XX:MaxDirectMemorySize,默认与堆最大值相等;超出即抛 OOM: Direct buffer memory。
- Netty 泄漏检测等级:-Dio.netty.leakDetection.level,国内习惯先开到 advanced,压测 5 min 就能出日志。
- 引用计数规则:retain() +1,release() –1,一旦 JVM 栈退出仍 >0 即泄漏;Unpooled 与 Pooled 的回收路径不同。
- Linux 进程级指标:top 的 RES、/proc/<pid>/smaps 中的 Anonymous 段、RSS 连续上涨而 JVM 堆 flat,即可定性为堆外。
- pmap –x 能按 KB 粒度列出所有段;若大量 64 MB 或 16 MB 的 anon 段持续增加,Netty 池化嫌疑最大。
- NMT(Native Memory Tracking)summary + detail.diff 可给出 Internal + Other 区上涨曲线,定位到“DirectByteBuffer.allocateMemory”还是“malloc”调用。
- jemalloc + jeprof 是国内阿里、字节、美团生产环境标配,–-enable-prof 编译后,通过 MALLOC_CONF=prof:true 采样,10% 性能损耗即可拿到火焰图,一眼看出 malloc 热点是否落在 .so 中的 JNI 函数。
- gdb 脚本:给 malloc/free 打断点,bt 打印 Java 栈,若栈底是 Native 方法,则 JNI 泄漏;若栈底是 java.nio.DirectByteBuffer.<init>,则 Netty 未释放。
- 国内云厂商(阿里云、腾讯云)已把上述工具做成“一键诊断”插件,面试时提到“可提工单开插件”体现实战落地。
答案
线上出现“堆外内存持续上涨、JVM 堆平稳”时,按“先定量、再定性、后归因”三步走,30 分钟内给出泄漏方是 Netty 还是 JNI。
-
定量:
a. 持续 3 min 采样 top –p <pid> 的 RES 与 /proc/<pid>/status 的 VmRSS,确认上涨斜率 >5 MB/min;
b. jcmd <pid> VM.native_memory summary.diff 对比基线,若 “Internal” 或 “Other” 区同步上涨,且 “DirectByteBuffer” 区占 80% 以上,则进入 Netty 分支;若 “malloc” 区占大头,则进入 JNI 分支。 -
定性:
Netty 分支:- 启动参数追加 -Dio.netty.leakDetection.level=advanced -Dio.netty.leakDetection.targetRecords=10,压测 5 min;
- grep “LEAK:” 应用日志,若出现 “Recent access records” 且栈顶为 ByteBuf.retain(),即可定位到业务 Handler;
- 若日志干净,则再用 jmap –histo:live 查看是否有大量 io.netty.buffer.PoolChunk,若其对象数随 RSS 同步增加,说明池化内存未归还,此时在代码里搜索未配对的 retain() 或忘记 release()。
JNI 分支:
- 安装 jemalloc 5.2.1 带 profiling 版本,重启进程加入 MALLOC_CONF=prof:true,lg_prof_sample:19;
- 运行 10 min 后 jeprof --show_bytes --pdf /path/java jeprof*.heap > leak.pdf,若热点函数落在 libxxx.so 中的 Java_com_xxx_nativeMethod,则 JNI 泄漏坐实;
- 若无法重启,用 gdb –batch –p <pid> –ex “set pagination off” –ex “b malloc” –ex “commands bt continue”,连续 200 次采样,若 70% 以上栈底是 JNI 函数,同样可定性。
-
归因与修复:
- Netty 泄漏:在 finally 块增加 ReferenceCountUtil.release(buf);若使用了 Unpooled,可改为 Pooled 并检查 SimpleLeakAwareByteBuf 的日志;
- JNI 泄漏:在 native 代码里补 free()/DeleteLocalRef(),并重新编译 so;上线前用 Valgrind memcheck 跑回归,确保 “definitely lost” 为 0。
交付物:一份 3 页报告,含 RSS 曲线图、NMT diff、jeprof 火焰图或 Netty LEAK 日志截图,给出“泄漏方 + 代码行 + 修复版本”,评审通过即闭环。
拓展思考
- 如果应用跑在容器(Docker)内,cgroup memory.limit_in_bytes 已打满,但宿主机 RES 仍涨,说明页缓存也被堆外泄漏污染,此时需同步 drop_caches 并对比 slabtop 的 kmalloc-4096 占用,避免误判。
- JDK 17 引入 Foreign Memory API(Panama),后期堆外泄漏可能不再经过 DirectByteBuffer,而是 MemorySegment;面试可主动提及“后续会用 jextract 生成头文件 + linker 采样”,展示技术前瞻性。
- 国内金融场景对 SLA 要求 4 个 9,泄漏问题从发现到止血必须 <15 min,可在预发布环境常驻 jemalloc profiling,采样率动态可调,配合阿里云 ARMS 告警实现 “RSS 连续 3 周期上涨 8% 即自动 dump”,把定位时间缩到 5 min 以内。