如何在读取 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.

中不小心使用线程局部变量造成的