如何在读取 JRT 后释放所有资源?
How to free all resources after reading a JRT?
我正在尝试读取给定 Java 9+ 安装中可用的模块列表,给定其 Java 主页,使用 .[=13 中描述的方法=]
该解决方案有效,但分配用于读取 Java 运行时映像内容的资源似乎从未被释放,导致内存泄漏,例如可通过 VisualVM 观察到:
如何修复以下复制中的内存泄漏?
package leak;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
public class JrtfsLeak {
public static void main(String[] args) throws Exception {
Path javaHome = Paths.get(args[0]);
for (int i = 0; i < 100000; ++i) {
modules(javaHome).close();
}
}
private static Stream<Path> modules(Path javaHome) throws Exception {
Map<String, String> env = Collections.singletonMap("java.home", javaHome.toString());
Path jrtfsJar = javaHome.resolve("lib").resolve("jrt-fs.jar");
try (URLClassLoader classloader = new URLClassLoader(new URL[] { jrtfsJar.toUri().toURL() })) {
try (FileSystem fs = FileSystems.newFileSystem(URI.create("jrt:/"), env, classloader)) {
Path modulesRoot = fs.getPath("modules");
return Files.list(modulesRoot);
}
}
}
}
参考javadoc方法list(在classjava.nio.file.Files
中)。这是相关部分。
API Note:
This method must be used within a try-with-resources statement or similar control structure to ensure that the stream's open directory is closed promptly after the stream's operations have completed.
换句话说,您需要关闭 modules
方法返回的 Stream
。
请注意,当您 运行 在 Java 9 或更高版本时,底层实现会在您指定 java.home
选项。因此,这些 class 不是由您的 URLClassLoader
加载的,而是由不同的 class 加载程序加载的。当不需要支持 9 之前的版本时,可以省略 class 加载器的创建。
在任何一种情况下,它们都由自定义 class 加载程序加载,并且可以在垃圾收集器支持时卸载。但是 class jdk.internal.jimage.ImageBufferCache
包含:
private static final ThreadLocal<BufferReference[]> CACHE =
new ThreadLocal<BufferReference[]>() {
@Override
protected BufferReference[] initialValue() {
// 1 extra slot to simplify logic of releaseBuffer()
return new BufferReference[MAX_CACHED_BUFFERS + 1];
}
};
如 中所述,从值到线程本地的反向引用可以防止其垃圾收集,并且当线程本地存储在 static
变量中时,对其中一个的引用class由相同的 class 加载器加载的 es 就足够了。
而这里的值是一个 BufferReference
的数组,这意味着即使该数组的所有条目都已被清除,数组类型本身也隐式引用了 class 的加载器那个文件系统。
但由于它是一个线程局部变量,我们可以通过让关键线程死掉来解决它。当我将您的代码更改为
public static void main(String[] args) throws InterruptedException {
Path javaHome = Paths.get(args[0]);
Runnable r = () -> test(javaHome);
for(int i = 0; i < 1000; ++i) {
Thread thread = new Thread(r);
thread.start();
thread.join();
}
}
static void test(Path javaHome) {
for (int i = 0; i < 1000; ++i) {
try(var s = modules(javaHome)) {}
catch(IOException ex) {
throw new UncheckedIOException(ex);
}
catch(Exception ex) {
throw new IllegalStateException(ex);
}
}
}
classes 被卸载。
这是一个 JDK 错误 JDK-8260621,已在 JDK 17 中修复。
这是由于 ImageBufferCache
.
中不小心使用线程局部变量造成的
我正在尝试读取给定 Java 9+ 安装中可用的模块列表,给定其 Java 主页,使用 该解决方案有效,但分配用于读取 Java 运行时映像内容的资源似乎从未被释放,导致内存泄漏,例如可通过 VisualVM 观察到: 如何修复以下复制中的内存泄漏?package leak;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Stream;
public class JrtfsLeak {
public static void main(String[] args) throws Exception {
Path javaHome = Paths.get(args[0]);
for (int i = 0; i < 100000; ++i) {
modules(javaHome).close();
}
}
private static Stream<Path> modules(Path javaHome) throws Exception {
Map<String, String> env = Collections.singletonMap("java.home", javaHome.toString());
Path jrtfsJar = javaHome.resolve("lib").resolve("jrt-fs.jar");
try (URLClassLoader classloader = new URLClassLoader(new URL[] { jrtfsJar.toUri().toURL() })) {
try (FileSystem fs = FileSystems.newFileSystem(URI.create("jrt:/"), env, classloader)) {
Path modulesRoot = fs.getPath("modules");
return Files.list(modulesRoot);
}
}
}
}
参考javadoc方法list(在classjava.nio.file.Files
中)。这是相关部分。
API Note:
This method must be used within a try-with-resources statement or similar control structure to ensure that the stream's open directory is closed promptly after the stream's operations have completed.
换句话说,您需要关闭 modules
方法返回的 Stream
。
请注意,当您 运行 在 Java 9 或更高版本时,底层实现会在您指定 java.home
选项。因此,这些 class 不是由您的 URLClassLoader
加载的,而是由不同的 class 加载程序加载的。当不需要支持 9 之前的版本时,可以省略 class 加载器的创建。
在任何一种情况下,它们都由自定义 class 加载程序加载,并且可以在垃圾收集器支持时卸载。但是 class jdk.internal.jimage.ImageBufferCache
包含:
private static final ThreadLocal<BufferReference[]> CACHE =
new ThreadLocal<BufferReference[]>() {
@Override
protected BufferReference[] initialValue() {
// 1 extra slot to simplify logic of releaseBuffer()
return new BufferReference[MAX_CACHED_BUFFERS + 1];
}
};
如 static
变量中时,对其中一个的引用class由相同的 class 加载器加载的 es 就足够了。
而这里的值是一个 BufferReference
的数组,这意味着即使该数组的所有条目都已被清除,数组类型本身也隐式引用了 class 的加载器那个文件系统。
但由于它是一个线程局部变量,我们可以通过让关键线程死掉来解决它。当我将您的代码更改为
public static void main(String[] args) throws InterruptedException {
Path javaHome = Paths.get(args[0]);
Runnable r = () -> test(javaHome);
for(int i = 0; i < 1000; ++i) {
Thread thread = new Thread(r);
thread.start();
thread.join();
}
}
static void test(Path javaHome) {
for (int i = 0; i < 1000; ++i) {
try(var s = modules(javaHome)) {}
catch(IOException ex) {
throw new UncheckedIOException(ex);
}
catch(Exception ex) {
throw new IllegalStateException(ex);
}
}
}
classes 被卸载。
这是一个 JDK 错误 JDK-8260621,已在 JDK 17 中修复。
这是由于 ImageBufferCache
.