Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

`Native.typeOptions` never releases library interfaces, leaking classloaders

未关闭
#1,742 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

@curforever 已经在做这个了。

开始于 2026年10月12日。

  • #1743 来自 @curforever —— 未关闭

评估

难度
4/5
预计耗时
1-2 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
java
领域
backend

调研方向

Start in src/com/sun/jna/Native.java, especially cacheOptions, getLibraryOptions, and findEnclosingLibraryClass; read the WeakHashMap and ClassValue behavior described in the issue. Reproduce the leak with the attached project’s ./gradlew test and inspect how typeOptions retains child classloader values. Done means the test can collect discarded script classloaders while preserving library option behavior.

由索引模型根据 Issue 内容生成。

描述

Summary

When JNA is loaded by a long-lived classloader and Native.load(name, SomeInterface.class) is called from a shorter-lived child classloader, the child classloader can never be garbage-collected.

Every time the host replaces the child, one more copy of its classes is leaked. This happens even after the child has dropped every reference to the proxy, and there is no public API to release it.

A Gradle build script is an everyday example. JNA on the buildscript classpath lives in one classloader. The script body is compiled into a child classloader, which Gradle replaces every time the script is edited.

Cause

Native.typeOptions is a WeakHashMap<Class<?>, Map<String, Object>>, so entries are meant to go away with their class. But Native.cacheOptions stores the key in its own value:

private static Map<String, Object> cacheOptions(Class<?> cls, Map<String, ?> options, Object proxy) {
    Map<String, Object> libOptions = new HashMap<>(options);
    libOptions.put(_OPTION_ENCLOSING_LIBRARY, cls);   // value -> key
    typeOptions.put(cls, libOptions);
    ...

https://github.com/java-native-access/jna/blob/5.19.1/src/com/sun/jna/Native.java#L2079-L2101

The WeakHashMap Javadoc warns about this.

The value objects in a WeakHashMap are held by ordinary strong references. Thus care should be taken to ensure that value objects do not strongly refer to their own keys, either directly or indirectly, since that will prevent the keys from being discarded.

The interface class is reachable from the static map, through its own value, so its classloader cannot be collected. A heap dump of the reproducer shows the path for every discarded script:

Native.typeOptions -> WeakHashMap$Entry.value -> HashMap["enclosing-library"] -> Build_gradle$LibC class

The nested-type entries (typeOptions.put(type, libraryOptions) in getLibraryOptions) pin the same way. Their value holds the enclosing library's class, which comes from the same classloader as the key. So do any caller-supplied option values from the child classloader, such as a TypeMapper.

Reproducer

project.zip

JNA 5.19.1, Gradle 9.8.1, JDK 17.0.20.1.

Run ./gradlew test

Failure:

> Task :test
ScriptLeakTest > discarded script classloaders are collected(Path) FAILED
    org.opentest4j.AssertionFailedError: 10 discarded classloaders, 10 copies of Build_gradle$LibC still loaded, 1 live 'JNA Cleaner' threads ==> expected: <true> but was: <false>
        at app//org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:199)
        at app//repro.ScriptLeakTest.discarded script classloaders are collected(ScriptLeakTest.kt:40)

1 test completed, 1 failed

> Task :test FAILED

FAILURE: Build failed with an exception.

The (somewhat artificial) project that is is tested is in examples/script/build.gradle.kts:

import com.sun.jna.Library
import com.sun.jna.Native

buildscript {
  dependencies {
    classpath("net.java.dev.jna:jna:5.19.1")
  }
}

plugins {
  base
}

// The test in ScriptLeakTest.kt changes this line before each build, as editing the script would.
val edit = 0

interface LibC : Library {
  fun getpid(): Int
}

val libc: LibC = Native.load("c", LibC::class.java)

tasks.register("scriptInfo") {
  doLast {
    logger.quiet("daemon pid ${libc.getpid()}")
  }
}

ScriptLeakTest re-runs the build 10 times, and edits val edit = 0 to a new value. Each build re-uses the same Gradle daemon. The result is a new classloader for each edit (as expected). The test counts the loaded copies of LibC with jcmd VM.class_hierarchy (after forcing a GC).

  • Expectation: Buildscript classloaders from previous builds are GC'd.
  • Actual: Buildscript classloaders from previous builds are strongly held by JNA, and cannot be GC'd.

(Note: Run the Gradle daemon with -XX:SoftRefLRUPolicyMSPerMB=0 so a full GC also clears soft references (which is how Gradle caches buildscript classloaders). Without the flag those look like a leak too, even in a build that doesn't use JNA.

Suggestions

Use ClassValue. A ClassValue's value is held by the class itself, so a value that refers back to the class, or to its classloader, doesn't keep it alive. That covers the enclosing library's class and any caller-supplied option values from the child classloader, such as a custom TypeMapper. Native.libraries has the same shape, though it is already safe there because the proxy is held weakly.

ClassValue has no put: it only computes a value on the first get, through computeValue. JNA writes to typeOptions explicitly from cacheOptions and getLibraryOptions, so the switch isn't drop-in. It would need something like ClassValue<AtomicReference<Map<String, Object>>>, and set after get.

Alternatively, store _OPTION_ENCLOSING_LIBRARY as a WeakReference<Class<?>> and unwrap it in findEnclosingLibraryClass. That fixes the default case, Native.load without options, but not option values defined in the child classloader, such as a custom TypeMapper, which would still keep it alive.

主要语言
Java
星标
8.9k
派生
1.7k
平均合并
1 天 13 小时
30 天内合并 PR
1

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

java-native-access/jna 的其他 Issue

查看 java-native-access/jna 的全部 Issue

相似的 Issue

更多 Java Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。