JUnit与JNA结合场景下仅含asan及未知模块的内存泄漏报告成因探究
JUnit与JNA结合场景下仅含asan及未知模块的内存泄漏报告成因探究
首先得说,你这个问题的实验设计做得非常细致——通过调整测试顺序、简化代码、对比不同场景的ASAN输出,已经把范围缩得很准了。结合你给出的所有细节,这种「仅显示asan和<unknown module>」的泄漏报告,核心原因是JNA的Native库被提前卸载,同时GC没来得及回收Memory对象,导致ASAN在进程退出时无法解析已卸载模块的符号。下面一步步拆解:
1. 核心矛盾:Native库卸载 vs GC回收时机
你观察到的两个关键现象已经指向了这个方向:
- 当
find测试在92个dummy测试之后运行时,报告全是<unknown module>;如果把find放在最前面跑,就能看到libjvm.so的调用栈。 - 减少dummy测试数量,或者把
stream+toArray改成直接创建数组,泄漏报告就消失了。
这里的逻辑链是这样的:
- JNA的Native实现(比如
libjnidispatch.so)是和类加载器绑定的。当你运行大量重复的dummy测试时,JVM会触发类加载器回收(OpenJDK 8的元空间回收阈值很容易被大量重复测试触发),JNA的类加载器被标记为可回收后,对应的Native库会被dlclose卸载。 - 而
find测试里,通过stream+toArray创建的Memory对象,其引用生命周期被延长了——stream的中间操作会临时持有引用,导致GC没有及时回收这些对象,它们的finalizer(JNA的Memory重写了finalize方法,用来释放Native内存)也没机会执行。 - 当ASAN在进程退出时扫描泄漏,这些
Memory对应的Native内存还没被释放,而对应的JNA Native库已经被卸载了。ASAN无法从已卸载的库中解析符号,所以调用栈里只能显示ASAN自己的malloc调用,后面的全部变成<unknown module>。
2. 为什么stream+toArray和dummy测试数量会影响?
stream+toArray的作用:stream的map操作会把所有Memory对象存在内部的数组里,直到toArray执行完成才会释放临时引用。相比直接创建数组,这个过程中引用的持有时间更长,更不容易被GC及时回收。而直接创建数组的话,引用生命周期更短,GC能在Native库卸载前就回收对象,执行finalize释放内存,ASAN就不会报泄漏。- dummy测试数量的影响:JVM的类卸载需要达到一定的阈值(比如元空间使用量、类加载次数)。92个dummy测试刚好触发了这个阈值,让JNA的类加载器被回收;如果减少数量,阈值没到,Native库不会被卸载,ASAN就能解析出正常的调用栈。
3. 验证与缓解方案
验证方法
你可以通过以下操作验证这个结论:
- 在
find测试的最后加上System.gc()(或者Runtime.getRuntime().gc()),强制触发GC,看泄漏报告是否消失或者出现正常的符号。 - 给JVM加上参数
-Xnoclassgc,禁用类卸载功能,再跑测试,此时无论测试顺序如何,ASAN都应该能解析出正常的调用栈。 - 手动释放
Memory:把map里的逻辑改成用try-with-resources(JNA 5.9+的Memory实现了AutoCloseable),或者显式调用ptr.dispose(),这样即使GC没触发,也能主动释放Native内存,ASAN不会报泄漏。
缓解方案
如果要解决这个问题(或者至少让你能写出有效的ASAN抑制规则),可以这么做:
手动管理Native内存生命周期:
对于JNA的Memory,尽量用显式释放的方式,比如:list.stream().map(arg -> { try (Memory ptr = new Memory(1024 * 1024)) { // 这里使用ptr return ptr.share(); // 或者根据需求处理引用 } }).toArray(Pointer[]::new);或者在不需要
Memory的时候主动调用dispose()。调整JVM参数:
- 加上
-Xnoclassgc禁用类卸载,避免JNA的Native库被提前卸载,但这可能会增加元空间的使用量。 - 调整GC参数,比如
-XX:+ExplicitGCInvokesConcurrent或者调整-XX:MaxMetaspaceSize,让GC和类卸载的时机更可控。
- 加上
编写精准的ASAN抑制规则:
你提到调用栈的#0-#5地址后缀是固定的,即使有ASLR也不会变。可以利用这一点编写抑制规则,比如:leak:match(fun, "malloc") leak:match(addr, "0x.*b372") leak:match(addr, "0x.*85e6") leak:match(addr, "0x.*7bcf") leak:match(addr, "0x.*80f5") leak:match(addr, "0x.*7e3f")这样即使是
<unknown module>的情况,也能精准抑制这个特定的泄漏报告。
备注:内容来源于stack exchange,提问作者Xellos
相关产品推荐
相关产品推荐

