读完一篇JRT后如何释放所有资源?

Mar*_*tin 6 java jvm memory-leaks

我正在尝试使用如何提取文件 jre-9/lib/modules?.

该解决方案有效,但似乎分配给读取 Java 运行时映像内容的资源从未被释放,从而导致内存泄漏,例如可以通过 VisualVM 观察到:

使用 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);
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

apa*_*gin 5

这是一个 JDK 错误JDK-8260621,已在 JDK 17 中修复。
这是由于在ImageBufferCache.


Hol*_*ger 4

请注意,当您在 Java\xc2\xa09 或更高版本下运行时,底层实现将为jrt-fs.jar您指定java.home选项时创建一个新的类加载器。因此,这些类不是由您加载的URLClassLoader,而是由不同的类加载器加载的。当您不需要 xe2x80x99 9 之前的版本的支持时,您可以省略类加载器的创建。

\n

无论哪种情况,它们\xe2\x80\x99都由自定义类加载器加载,并且可以在垃圾收集器支持时卸载。但该类jdk.internal.jimage.ImageBufferCache包含:

\n
private static final ThreadLocal<BufferReference[]> CACHE =\n    new ThreadLocal<BufferReference[]>() {\n        @Override\n        protected BufferReference[] initialValue() {\n            // 1 extra slot to simplify logic of releaseBuffer()\n            return new BufferReference[MAX_CACHED_BUFFERS + 1];\n        }\n    };\n
Run Code Online (Sandbox Code Playgroud)\n

How does this ThreadLocal Prevent the Classloader from getting GCed中所述,从值到线程本地的反向引用可以阻止其垃圾回收,并且当线程本地存储在变量中时static,对同一线程加载的类之一的引用类加载器就足够了。

\n

在这里,该值是一个 数组BufferReference,这意味着即使该数组的所有条目都已被清除,数组类型本身也具有对该文件系统的类加载器的隐式引用。

\n

但由于它是一个线程局部变量,我们可以通过让关键线程死亡来解决它。当我将你的代码更改为

\n
public static void main(String[] args) throws InterruptedException {\n    Path javaHome = Paths.get(args[0]);\n    Runnable r = () -> test(javaHome);\n    for(int i = 0; i < 1000; ++i) {\n        Thread thread = new Thread(r);\n        thread.start();\n        thread.join();\n    }\n}\n\nstatic void test(Path javaHome) {\n    for (int i = 0; i < 1000; ++i) {\n        try(var s = modules(javaHome)) {}\n        catch(IOException ex) {\n            throw new UncheckedIOException(ex);\n        }\n        catch(Exception ex) {\n            throw new IllegalStateException(ex);\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

类被卸载。

\n