在运行时使用JDK编译器时内存泄漏

时间:2013-01-31 02:11:52

标签: reflection classloader java

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

在我的程序中,类被加载,构造函数被执行并且类被卸载(没有剩余的实例,并且当我将指针设置为null时,classLoader变得无效)。我用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();
}

}

5 个答案:

答案 0 :(得分:6)

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

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

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

  1. 启动应用程序并获取编译器和ScriptFileManager,然后执行GC-finalize-GC。转储。
  2. 加载500个'脚本'。
  3. 进行GC-finalize-GC。写下统计数据。转储。
  4. 加载500个'脚本'。
  5. 进行GC-finalize-GC。写下统计数据。转储。
  6. 显而易见(双倍)实例数增加:Names(500 - > 1k),SharedNameTable(500&gt; 1k),SharedNameTable$NameImpl(数十万)和[LSharedNameTable$NameImpl(500-> 1k)。

    在使用EMA进行分析后,很明显SharedNameTablecom.sun.tools.javac.util.List的静态引用显然SoftReference每个SharedNameTable创建了一个$NameImpl您在运行时编译的源文件)。所有SharedNameTable s都是您的源文件被拆分为的标记。而且显然所有的令牌都从不从堆中解放出来并积累到无止境......或者他们是吗?

    我决定测试这是否真的如此。知道了软引用和弱引用的区别,我决定使用一个小堆(-Xms32m -Xmx32m)。这样JVM就被强制释放OutOfMemoryError或失败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. 。结果不言而喻:

    -Xmx512m -Xms512m

    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.
    

    -Xmx32m -Xms32m

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

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

    脚本:

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

    doNotCollect:

    {{1}}

答案 1 :(得分:2)

  

当我将类加载器设置为null时,类定义被卸载。和垃圾收集。 JConsole还告诉我这些类是卸载的。加载的总类数返回初始值。

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

  

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

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

答案 2 :(得分:2)

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

答案 3 :(得分:1)

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

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

答案 4 :(得分:1)

正如其他答案已经指出的那样,问题是编译器将SoftReference保持在SharedNameTable左右。

Chrispy提到了-XDuseUnsharedTable javac选项。因此,最后遗漏的一点是如何在使用Java API时启用此选项:

compiler.getTask(null, fileManager, null, Arrays.asList("-XDuseUnsharedTable"), null, units)