8

我正在尝试将 javaeditor 添加到我的程序中以在运行时扩展程序。一切正常,除非广泛使用该程序(我模拟了 1000-10000 次编译器执行)。内存使用率越来越高,看起来好像有内存泄漏。

在我的程序中,类被加载,构造函数被执行并且类被卸载(没有剩余的实例并且类加载器变得无效,因为我将指针设置为空)。我用 JConsole 分析了这个过程,当垃圾收集器被执行时,这些类被卸载了。

我做了一个 heapdum 在内存分析器中打开它,问题似乎出在 java.net.FactoryURLClassLoader 内部(在 com.sun.tools.javac.util.List 对象中)。由于 (com.sun.tools.javac) 是 JDK 的一部分,而不是 JRE 的一部分,并且 SystemToolClassLoader 是一个 FactoryURLClassLoader 对象,因此我会在某处找到泄漏。当我第一次执行编译器时,SystemToolClassLoader 中加载的类数从 1 增加到 521,但之后保持不变。

所以我不知道泄漏在哪里,有没有办法重置 SystemToolClassLoader?我怎样才能更准确地定位泄漏。

编辑:好的,我发现它也出现在一个非常简单的例子中。所以它似乎是编译的一部分,我不需要加载类或实例化它:

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;


public class Example {   

public static void main(String[] args)
{
    for (int i =0; i<10000;i++){
        try {
            System.out.println(i);
            compile();
        } catch (InstantiationException | IllegalAccessException
                | ClassNotFoundException | IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}

public static void compile() throws IOException, InstantiationException, IllegalAccessException, ClassNotFoundException
{
    File source = new File( "src\\Example.java" ); // This File
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
    Iterable<? extends JavaFileObject> units;
    units = fileManager.getJavaFileObjectsFromFiles( Arrays.asList( source ) );
    compiler.getTask( null, fileManager, null, null, null, units ).call();
    fileManager.close();
}

}
4

5 回答 5

6

起初,我认为这是一个明确的内存泄漏;SoftReference但是,它与s 的工作方式直接相关。

Oracle 的 JVM 只会在堆完全用完时尝试收集软引用。似乎不可能强制以编程方式收集软引用。

为了确定问题,我在“无限”堆上使用了三个转储:

  1. 启动应用程序并获取 Compiler 和 ScriptFileManager,然后执行 GC-finalize-GC。做一个转储。
  2. 加载 500 个“脚本”。
  3. 做一个 GC-finalize-GC。写统计。做一个转储。
  4. 加载 500 个“脚本”。
  5. 做一个 GC-finalize-GC。写统计。做一个转储。

明显的(双)实例计数增加:Names(500 -> 1k),SharedNameTable(500->1k),SharedNameTable$NameImpl(数十万)和[LSharedNameTable$NameImpl(500->1k)。

在使用 EMA 进行分析之后,很明显它SharedNameTable有一个对 a 的静态引用,com.sun.tools.javac.util.List这显然SoftReference是每个SharedNameTable曾经创建的文件(因此,对于您在运行时编译的每个源文件都有一个)。所有的$NameImpls 都是你的源文件被分割成的标记。显然,所有的令牌都永远不会从堆中释放出来,并且会不断累积……或者是吗?

我决定测试是否真的是这样。知道软引用和弱引用的区别后,我决定使用一个小堆(-Xms32m -Xmx32m)。这样 JVM 被迫要么释放SharedNameTables 要么失败OutOfMemoryError。结果不言自明:

-Xmx512m -Xms512m

Total memory: 477233152
Free memory: 331507232
Used memory: 138.97506713867188 MB
Loaded scripts: 500

Total memory: 489816064
Free memory: 203307408
Used memory: 273.23594665527344 MB
Loaded scripts: 1000

The classloader/component "java.net.FactoryURLClassLoader @ 0x8a8a748" occupies 279.709.192 (98,37%) bytes.

-Xmx32m -Xms32m

Total memory: 29687808
Free memory: 25017112
Used memory: 4.454322814941406 MB
Loaded scripts: 500

Total memory: 29884416
Free memory: 24702728
Used memory: 4.941642761230469 MB
Loaded scripts: 1000

One instance of "com.sun.tools.javac.file.ZipFileIndex" loaded by "java.net.FactoryURLClassLoader @ 0x8aa4cc8" occupies 2.230.736 (47,16%) bytes. The instance is referenced by *.*.script.ScriptFileManager @ 0x8ac8230.

(这只是一个指向 JDK 库的链接。)

脚本:

public class Avenger
{
    public Avenger()
    {
        JavaClassScriptCache.doNotCollect(this);
    }

    public static void main(String[] args)
    {
        // this method is called after compiling
        new Avenger();
    }
}

不收集:

private static final int TO_LOAD = 1000;
private static final List<Object> _active = new ArrayList<Object>(TO_LOAD);

public static void doNotCollect(Object o)
{
    _active.add(o);
}

System.out.println("Loaded scripts: " + _active.size());
于 2013-04-18T09:37:21.413 回答
2

当我将类加载器设置为空时,类定义被卸载。和垃圾收集。JConsole 还告诉我这些类已卸载。加载的总类恢复到初始值。

这是非常有说服力的证据,表明这不是经典的类加载器泄漏。

eclipse 内存分析器也认为它是一个 com.sun.tools.javac.util.List 对象,它占用内存....所以它在堆上

下一步应该是确定对该 List 对象的引用(或引用)在哪里。运气好的话,您可以查看源代码以查找列表对象的用途,以及是否有某种方法可以将其清除。

于 2013-01-31T02:37:32.247 回答
2

Java 7 引入了这个错误:为了加快编译速度,他们引入了 SharedNameTable,它使用软引用来避免重新分配,但不幸的是只会导致 JVM 膨胀失控,因为这些软引用在 JVM 之前永远不会被回收达到其-Xmx内存限制。据称它将在 Java 9 中修复。与此同时,有一个(未记录的)编译器选项可以禁用它:-XDuseUnsharedTable.

于 2016-07-21T20:11:55.110 回答
1

这不是内存泄漏,它就像“由于内存可用并且有一些有用的东西要保留,直到真的需要摆脱和释放内存,我会为你保留编译好的源代码”。--编译器说

基本上编译器工具(这里使用的内部编译器工具)保留对已编译源的引用,但它保留为软引用。这意味着如果 JVM 倾向于使用我们的内存,垃圾收集器将要求它保留的内存。尝试以最小的堆大小运行您的代码,您将看到正在清理的引用。

于 2013-10-14T07:24:25.927 回答
1

正如其他答案已经指出的那样,问题在于编译器将SoftReferences保持在SharedNameTable附近。

Chrispy 提到了-XDuseUnsharedTablejavac 选项。所以最后缺少的一点是如何在使用 Java API 时启用此选项:

compiler.getTask(null, fileManager, null, Arrays.asList("-XDuseUnsharedTable"), null, units)
于 2017-06-15T12:15:00.037 回答