我正在尝试向我的程序添加一个javaeditor,以便在运行时扩展程序。一切都很好,除了广泛使用该程序时(我模拟了 1000-10000 次编译器执行)。内存使用量上升和上升,看起来有内存泄漏。
在我的程序中,类被加载,构造函数被执行,类被卸载(没有剩余的实例,当我将指针设置为 null 时,classLoader 变得无效)。我用JConsole分析了这个过程,当垃圾收集器被执行时,类被卸载。
我在内存分析器中打开了它,问题似乎出在java.net.FactoryURLClassLoader内部(在com.sun.tools.javac.util.List对象中)。由于(com.sun.tools.javac)是JDK的一部分,而不是在JRE中,并且SystemToolClassLoader是一个FactoryURLClassLoader对象,我会在那里找到泄漏。当我第一次执行编译器时,SystemToolClassLoader 中加载的类的数量从 1 增加到 521,但之后保持不变。
所以我不知道泄漏在哪里,有没有办法重置系统工具类加载器?我怎样才能更精确地定位泄漏。
编辑:好的,我发现它也发生在一个非常简单的例子中。所以它似乎是编译的一部分,我不需要加载类或实例化它:
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();
}
}
起初,我认为这是一个明确的内存泄漏; 但是,它与SoftReference
的工作方式直接相关。
Oracle 的 JVM 只会在堆完全用完时才尝试收集软引用。似乎不可能强制以编程方式收集软引用。
为了确定问题,我在"无限"堆上使用了三个转储:
- 启动应用程序并获取编译器和脚本文件管理器,然后执行GC-finalize-GC。进行转储。
- 加载 500 个"脚本"。
- 执行 GC 最终确定的 GC。写入统计信息。进行转储。
- 加载 500 个"脚本"。
- 执行 GC 最终确定的 GC。写入统计信息。进行转储。
明显(双重)实例计数增加:Names
(500 -> 1k)、SharedNameTable
(500->1k)、SharedNameTable$NameImpl
(数十万)和[LSharedNameTable$NameImpl
(500->1k)。
使用 EMA 进行分析后,很明显SharedNameTable
对一个com.sun.tools.javac.util.List
有一个静态引用,该显然SoftReference
创建的每一个SharedNameTable
(因此,您在运行时编译的每个源文件都有一个)。所有$NameImpl
都是源文件拆分到的令牌。显然,所有的代币都不会从堆中释放出来,并且积累得无休止......还是他们?
我决定测试一下是否真的是这样。知道软引用与弱引用的区别后,我决定使用一个小堆(-Xms32m -Xmx32m)。这样,JVM要么被强制释放SharedNameTable
s,要么失败并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());
Java 7 引入了这个错误:为了加快编译速度,他们引入了 SharedNameTable,它使用软引用来避免重新分配,但不幸的是,这只会导致 JVM 膨胀失控,因为这些软引用永远不会被回收,直到 JVM 达到其-Xmx
内存限制。据称它将在Java 9中修复。同时,有一个(未记录的)编译器选项可以禁用它:-XDuseUnsharedTable
.
正如其他答案已经指出的那样,问题在于编译器保持SoftReference
SharedNameTable
。
Chrispy提到了javac选项-XDuseUnsharedTable
。因此,最后一个缺失的一点是如何在使用Java API时启用此选项:
compiler.getTask(null, fileManager, null, Arrays.asList("-XDuseUnsharedTable"), null, units)
当我将类加载器设置为 null 和垃圾回收时,类定义被卸载。JConsole还告诉我,这些类是未加载的。加载的类总数将返回到初始值。
这是非常令人信服的证据,证明这不是经典的类加载器泄漏。
此外,eclipse内存分析器认为它是一个com.sun.tools.javac.util.List对象,它占用了内存。所以它在堆上
下一步应该是确定对该 List 对象的引用(或多个引用)的位置。 运气好的话,你可以查看源代码,找到列表对象的用途,以及是否有某种方法可以清除它。
这不是内存泄漏,它就像"既然内存可用并且有一些有用的东西可以保留,直到真正需要摆脱和释放内存,我会为你保留编译后的源代码"。 --编译器说
基本上编译器工具(这里使用的内部编译器工具)保留对编译的源代码的引用,但它保留为软引用。这意味着如果 JVM 倾向于占用我们的内存,垃圾收集器将声明它保留的内存。尝试以最小的堆大小运行代码,您将看到正在清理的引用。