`Native.typeOptions` never releases library interfaces, leaking classloaders
Chưa có ai nhận issue này.
Đánh giá
- Độ khó
- 4/5
- Thời gian dự kiến
- 1-2 ngày
- Mức phù hợp với người mới
- 48/100
Hướng nghiên cứu
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.
Do mô hình lập chỉ mục viết ra từ nội dung của issue.
Mô tả
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
WeakHashMapare 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
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.
- Ngôn ngữ chính
- Java
- Star
- 8.9k
- Fork
- 1.7k
- Merge trung bình
- 1 ngày 13 giờ
- Pull request đã merge (30 ngày)
- 1
Chuẩn bị môi trường
Dự án này không cung cấp dev container, Dockerfile hay hướng dẫn đóng góp, nên bạn cần tự thiết lập môi trường: hãy bắt đầu từ README và xem hướng dẫn đóng góp lần đầu của chúng tôi để biết các bước chung.
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của java-native-access/jna
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 66/100
java-native-access/jna#1738 ·
-
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 45/100
java-native-access/jna#1736 ·
-
Độ khó 3/5 1-2 ngày Mức phù hợp với người mới 65/100
java-native-access/jna#1717 ·
-
Windows32Exception - The parameter is incorrectCó thể đã có người làm @marktech0813 đã nhận 336 ngày trước. Đang mở
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
java-native-access/jna#1700 · 11 bình luận ·
-
feature request
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 35/100
java-native-access/jna#1698 · 1 bình luận ·
Tất cả issue của java-native-access/jna
Issue tương tự
-
Flight SQL JDBC parses boolean properties case-sensitively, silently disabling TLS for useEncryption=TRUECó thể đã có người làm @Arawoof06 đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 79/100
apache/arrow-java#1357 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 77/100
apache/rocketmq-dashboard#6610 ·
Maintainer thường phản hồi trong vòng 3 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
MetricsHub/metricshub-community#1396 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
L: java:gradle L: java:maven L: ruby:bundler T: bug 🐞
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
dependabot/dependabot-core#16547 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Citationcount column sorts alphabetically instead of numericallyCó thể đã có người làm @kpunia306 đã nhận hôm nay. Đang mởstatus: needs-triage
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
JabRef/jabref#17459 · 4 bình luận ·
Maintainer thường phản hồi trong vòng 1 ngày