You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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抑制规则),可以这么做:

  1. 手动管理Native内存生命周期:
    对于JNA的Memory,尽量用显式释放的方式,比如:

    list.stream().map(arg -> {
        try (Memory ptr = new Memory(1024 * 1024)) {
            // 这里使用ptr
            return ptr.share(); // 或者根据需求处理引用
        }
    }).toArray(Pointer[]::new);
    

    或者在不需要Memory的时候主动调用dispose()。

  2. 调整JVM参数:

    • 加上-Xnoclassgc禁用类卸载,避免JNA的Native库被提前卸载,但这可能会增加元空间的使用量。
    • 调整GC参数,比如-XX:+ExplicitGCInvokesConcurrent或者调整-XX:MaxMetaspaceSize,让GC和类卸载的时机更可控。
  3. 编写精准的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.14 17:14:34