【问题标题】:How to free all resources after reading a JRT?阅读JRT后如何释放所有资源?
【发布时间】:2021-06-22 12:05:07
【问题描述】:

我正在尝试使用How to extract the file jre-9/lib/modules? 中描述的方法读取给定 Java 9+ 安装中可用的模块列表,给定它的 Java Home。

该解决方案有效,但分配用于读取 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);
      }
    }
  }
}

【问题讨论】:

  • 您使用的是哪个 Java 版本?
  • 我正在使用 Jabba 来管理我的 Java 安装。我使用了以下版本:采用@1.14.0-2、采用@1.11.0-7、采用@1.8.0-242
  • 好观察!这似乎是一个真正的 JDK 错误,由 ImageBufferCache 中不小心使用线程局部变量引起的。
  • @apangin 刚来the same conclusion

标签: java memory-leaks jvm


【解决方案1】:

这是一个 JDK 错误 JDK-8260621,已在 JDK 17 中修复。
这是由于ImageBufferCache 中不小心使用了线程局部变量造成的。

【讨论】:

    【解决方案2】:

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

    在任何一种情况下,它们都是由自定义类加载器加载的,并且可以在垃圾收集器支持时卸载。但是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];
            }
        };
    

    正如How does this ThreadLocal prevent the Classloader from getting GCed 中所解释的,从值到本地线程的反向引用可以防止其垃圾收集,并且当本地线程存储在static 变量中时,对同一类加载的类之一的引用loader就够了。

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

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

    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);
            }
        }
    }
    

    类被卸载。

    【讨论】:

      【解决方案3】:

      请参阅 javadoc 以了解方法 list(在类 java.nio.file.Files 中)。这是相关部分。

      API 说明:
      此方法必须在 try-with-resources 语句或类似的控制结构中使用,以确保在流的操作完成后立即关闭流的打开目录。

      换句话说,您需要关闭您的modules 方法返回的Stream

      【讨论】:

      • 感谢您指出,这是我在尽量减少复制时犯的错误。我更新了问题以关闭流,但这并不能解决问题,不幸的是:泄漏仍然存在。
      猜你喜欢
      • 1970-01-01
      • 2013-08-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-02-05
      • 2011-06-26
      相关资源
      最近更新 更多