8

プログラムに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に増加しますが、その後は同じままです。

リークがどこにあるのかわかりませんが、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

最初は、これは間違いなくメモリリークだと思いました。ただし、それはsの動作に直接関係していますSoftReference

OracleのJVMは、ヒープが完全に使い果たされた場合にのみソフト参照の収集を試みます。ソフト参照をプログラムで強制的に収集することはできないようです。

問題を特定するために、「無制限」ヒープで3つのダンプを使用しました。

  1. アプリを起動し、CompilerとScriptFileManagerを取得してから、GC-finalize-GCを実行します。ダンプを作成します。
  2. 500個の「スクリプト」をロードします。
  3. GC-finalize-GCを実行します。統計を書きます。ダンプを作成します。
  4. 500個の「スクリプト」をロードします。
  5. GC-finalize-GCを実行します。統計を書きます。ダンプを作成します。

明らかな(2倍の)インスタンス数の増加:Names(500-> 1k)、SharedNameTable(500-> 1k)、SharedNameTable$NameImpl(数十万)および[LSharedNameTable$NameImpl(500-> 1k)。

EMAで分析した後、これまでに作成されたすべてのもの(つまり、実行時にコンパイルしたソースファイルごとに1つ)への静的参照があることが明らかになりSharedNameTableますcom.sun.tools.javac.util.List。すべてのは、ソースファイルが分割されたトークンです。そして、どうやらすべてのトークンがヒープから解放されて終わりがないように蓄積されることはありません...それともそうですか?SoftReferenceSharedNameTable$NameImpl

これが実際に当てはまるかどうかをテストすることにしました。ソフト参照と弱参照の違いを知って、小さなヒープ(-Xms32m -Xmx32m)を使用することにしました。SharedNameTableこのようにして、JVMは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();
    }
}

doNotCollect:

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

クラスローダーをnullに設定すると、クラス定義がアンロードされます。とガベージコレクション。JConsoleは、これらのクラスがアンロードされていることも教えてくれます。ロードされたクラスの合計は初期値に戻ります。

これは、これが従来のクラスローダーリークではないというかなり説得力のある証拠です。

また、Eclipseメモリアナライザは、メモリを取得するcom.sun.tools.javac.util.Listオブジェクトであると見なします。したがって、ヒープ上にあります。

次のステップは、そのListオブジェクトへの参照がどこにあるかを特定することです。運が良ければ、ソースコードを調べて、リストオブジェクトが何に使用されているか、およびリストオブジェクトをクリアする方法があるかどうかを確認できます。

于 2013-01-31T02:37:32.247 に答える
2

Java 7はこのバグを導入しました。コンパイルを高速化するために、再割り当てを回避するためにソフト参照を使用するSharedNameTableを導入しましたが、残念ながら、これらのソフト参照はJVMまで再利用されない-Xmxため、JVMが制御不能になります。メモリ制限に達しました。伝えられるところでは、Java 9で修正される予定です。それまでの間、それを無効にする(文書化されていない)コンパイラオプションがあります-XDuseUnsharedTable

于 2016-07-21T20:11:55.110 に答える
1

これはメモリリークではなく、「メモリが利用可能であり、保持するのに役立つものがあるため、メモリを削除して解放する必要があるまで、コンパイルされたソースを保持します」のようなものです。-コンパイラは言う

基本的に、コンパイラツール(ここで使用されている内部コンパイラツール)は、コンパイルされたソースへの参照を保持しますが、ソフト参照として保持されます。つまり、JVMがメモリを使い果たす傾向がある場合、ガベージコレクタはガベージコレクタによって保持されているメモリを要求します。最小のヒープサイズでコードを実行してみてください。参照がクリーンアップされているのがわかります。

于 2013-10-14T07:24:25.927 に答える
1

他の回答がすでに指摘しているように、問題はコンパイラがSoftReferencesを維持することSharedNameTableです。

Chrispyは-XDuseUnsharedTablejavacオプションについて言及しました。したがって、最後に欠けているのは、JavaAPIを使用するときにこのオプションを有効にする方法です。

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