WeakHashMapを使用しているにもかかわらずOutOfMemoryException


9

を呼び出さない場合System.gc()、システムはOutOfMemoryExceptionをスローします。なぜSystem.gc()明示的に呼び出す必要があるのか​​わかりません。JVMはgc()それ自体を呼び出す必要がありますよね?お知らせ下さい。

以下は私のテストコードです:

public static void main(String[] args) throws InterruptedException {
    WeakHashMap<String, int[]> hm = new WeakHashMap<>();
    int i  = 0;
    while(true) {
        Thread.sleep(1000);
        i++;
        String key = new String(new Integer(i).toString());
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[1024 * 10000]);
        key = null;
        //System.gc();
    }
}

以下のように追加-XX:+PrintGCDetailsして、GC情報を印刷します。ご覧のとおり、実際には、JVMは完全なGC実行を試みますが失敗します。私はまだその理由を知りません。System.gc();行をコメント解除すると、結果がポジティブになるのは非常に奇妙です。

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
[GC (Allocation Failure) --[PSYoungGen: 48344K->48344K(59904K)] 168344K->168352K(196608K), 0.0090913 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC (Ergonomics) [PSYoungGen: 48344K->41377K(59904K)] [ParOldGen: 120008K->120002K(136704K)] 168352K->161380K(196608K), [Metaspace: 5382K->5382K(1056768K)], 0.0380767 secs] [Times: user=0.09 sys=0.03, real=0.04 secs] 
[GC (Allocation Failure) --[PSYoungGen: 41377K->41377K(59904K)] 161380K->161380K(196608K), 0.0040596 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (Allocation Failure) [PSYoungGen: 41377K->41314K(59904K)] [ParOldGen: 120002K->120002K(136704K)] 161380K->161317K(196608K), [Metaspace: 5382K->5378K(1056768K)], 0.0118884 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at test.DeadLock.main(DeadLock.java:23)
Heap
 PSYoungGen      total 59904K, used 42866K [0x00000000fbd80000, 0x0000000100000000, 0x0000000100000000)
  eden space 51712K, 82% used [0x00000000fbd80000,0x00000000fe75c870,0x00000000ff000000)
  from space 8192K, 0% used [0x00000000ff800000,0x00000000ff800000,0x0000000100000000)
  to   space 8192K, 0% used [0x00000000ff000000,0x00000000ff000000,0x00000000ff800000)
 ParOldGen       total 136704K, used 120002K [0x00000000f3800000, 0x00000000fbd80000, 0x00000000fbd80000)
  object space 136704K, 87% used [0x00000000f3800000,0x00000000fad30b90,0x00000000fbd80000)
 Metaspace       used 5409K, capacity 5590K, committed 5760K, reserved 1056768K
  class space    used 576K, capacity 626K, committed 640K, reserved 1048576K

jdkのバージョン -Xmsおよび-Xmxパラメーターを使用しますか?OOMを取得したのはどのステップですか?
Vladislav Kysliy

1
私のシステムではこれを再現できません。デバッグモードでは、GCがその役割を果たしていることがわかります。マップが実際にクリアされているかどうかをデバッグモードで確認できますか?
magicmn

jre 1.8.0_212-b10 -Xmx200m添付したgcログから詳細を確認できます。thx
ドミニクペン

回答:


7

JVMは独自にGCを呼び出しますが、この場合は遅すぎます。この場合、メモリをクリアするのはGCだけではありません。マップ値は強く到達可能であり、特定の操作がマップで呼び出されると、マップ自体によってクリアされます。

GCイベント(XX:+ PrintGC)をオンにした場合の出力は次のとおりです。

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
add new element 6
add new element 7
[GC (Allocation Failure)  2407753K->2400920K(2801664K), 0.0123285 secs]
[GC (Allocation Failure)  2400920K->2400856K(2801664K), 0.0090720 secs]
[Full GC (Allocation Failure)  2400856K->2400805K(2590720K), 0.0302800 secs]
[GC (Allocation Failure)  2400805K->2400805K(2801664K), 0.0069942 secs]
[Full GC (Allocation Failure)  2400805K->2400753K(2620928K), 0.0146932 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

値をマップに入力する最後の試行まで、GCはトリガーされません。

WeakHashMapは、マップキーが参照キューで発生するまで、古いエントリをクリアできません。また、マップキーはガベージコレクションされるまで参照キューでは発生しません。新しいマップ値のメモリ割り当ては、マップがそれ自体をクリアする機会がある前にトリガーされます。メモリ割り当てが失敗してGCがトリガーされると、マップキーが収集されます。しかし、それは遅すぎます-新しいマップ値を割り当てるために十分なメモリが解放されていません。ペイロードを減らすと、おそらく新しいマップ値を割り当てるのに十分なメモリがなくなり、古いエントリが削除されます。

別の解決策は、値自体をWeakReferenceにラップすることです。これにより、マップが独自に行うのを待たずに、GCがリソースをクリアできるようになります。出力は次のとおりです。

add new element 1
add new element 2
add new element 3
add new element 4
add new element 5
add new element 6
add new element 7
[GC (Allocation Failure)  2407753K->2400920K(2801664K), 0.0133492 secs]
[GC (Allocation Failure)  2400920K->2400888K(2801664K), 0.0090964 secs]
[Full GC (Allocation Failure)  2400888K->806K(190976K), 0.1053405 secs]
add new element 8
add new element 9
add new element 10
add new element 11
add new element 12
add new element 13
[GC (Allocation Failure)  2402096K->2400902K(2801664K), 0.0108237 secs]
[GC (Allocation Failure)  2400902K->2400838K(2865664K), 0.0058837 secs]
[Full GC (Allocation Failure)  2400838K->1024K(255488K), 0.0863236 secs]
add new element 14
add new element 15
...
(and counting)

ずっといい。


あなたの答えのThx、あなたの結論は正しいようです。ペイロードを1024 * 10000から1024 * 1000に削減しようとしていますが、コードは正常に機能します。しかし、私はまだあなたの説明をあまり理解していません。あなたの意味として、WeakHashMapからスペースを解放する必要がある場合、少なくとも2回はgcを実行する必要があります。最初の時間は、マップからキーを収集し、それらを参照キューに追加することです。2回目は値を収集することですか?しかし、あなたが提供した最初のログから、実際には、JVMはすでに完全なGCを2回取得しています。
ドミニクペン

「マップの値は強く到達可能であり、特定の操作が呼び出されたときにマップ自体によってクリアされます。」と言っています。彼らはどこから連絡できますか?
Andronicus

1
あなたの場合、2回のGC実行だけでは十分ではありません。まず、GCを1回実行する必要があります。ただし、次のステップでは、マップ自体との対話が必要になります。探す必要があるのは、java.util.WeakHashMap.expungeStaleEntries参照キューを読み取り、マップからエントリを削除するメソッドです。これにより、値に到達できなくなり、コレクションの対象となります。その後、GCの2回目のパスでメモリが解放されます。expungeStaleEntriesget / put / sizeなどの多くの場合、または通常マップで行うほとんどすべての場合に呼び出されます。それが問題です。
触手

1
@Andronicus、これはWeakHashMapの中で最も混乱する部分です。それは複数回カバーされました。stackoverflow.com/questions/5511279/...
触手

2
@Andronicus この回答、特に後半は、参考になるかもしれません。また、このQ&A ...
ホルガー

5

他の答えは確かに正しいです、私は私のものを編集しました。小さな補遺としてG1GC、とは異なりParallelGC、この動作は見られません。これはのデフォルトですjava-8

あなたは、私が少し(の下で実行するようにプログラムを変更した場合どうなるどう思いますかjdk-8-Xmx20m

public static void main(String[] args) throws InterruptedException {
    WeakHashMap<String, int[]> hm = new WeakHashMap<>();
    int i = 0;
    while (true) {
        Thread.sleep(200);
        i++;
        String key = "" + i;
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[512 * 1024 * 1]); // <--- allocate 1/2 MB
    }
}

それはうまくいきます。何故ですか?WeakHashMapエントリをクリアする前に、新しい割り当てが発生するだけの十分な余地をプログラムに与えるからです。そして、他の答えはすでにそれがどのように起こるかを説明しています。

さて、ではG1GC、状況が少し異なります。そのような大きなオブジェクトが割り当てられている場合(通常は1 MBの1/2以上)、これはと呼ばれますhumongous allocation。これが発生すると、並行 GCがトリガーされます。そのサイクルの一部として、若いコレクションがトリガーCleanup phaseされ、イベントがに投稿されるのを処理するが開始され、エントリReferenceQueueWeakHashMapクリアされます。

したがって、このコードの場合:

public static void main(String[] args) throws InterruptedException {
    Map<String, int[]> hm = new WeakHashMap<>();
    int i = 0;
    while (true) {
        Thread.sleep(1000);
        i++;
        String key = "" + i;
        System.out.println(String.format("add new element %d", i));
        hm.put(key, new int[1024 * 1024 * 1]); // <--- 1 MB allocation
    }
}

私はjdk-13で実行しG1GCます(はデフォルトです)

java -Xmx20m "-Xlog:gc*=debug" gc.WeakHashMapTest

以下はログの一部です。

[2.082s][debug][gc,ergo] Request concurrent cycle initiation (requested by GC cause). GC cause: G1 Humongous Allocation

これはすでに別のことをしています。があったため、concurrent cycle(アプリケーションの実行中に行われる)が開始されますG1 Humongous Allocation。この並行サイクルの一部として、若いGCサイクルを実行します(実行中にアプリケーションを停止します)。

 [2.082s][info ][gc,start] GC(0) Pause Young (Concurrent Start) (G1 Humongous Allocation)

その若い GCの一部として、それはまた、巨大な領域をクリアしますここに欠陥があります。


あなたは今では見ることができjdk-13、本当に大きなオブジェクトが割り当てられているときに、古い地域に積み上げるためにゴミを待ちませんが、トリガー同時日保存されたGCサイクルを、。jdk-8とは異なり。

DisableExplicitGCExplicitGCInvokesConcurrent意味しているのか、またはその意味を読んだりSystem.gc、呼び出しがSystem.gc実際に役立つ理由を理解したりすることができます。


1
Java 8はデフォルトでG1GCを使用しません。また、OPのGCログは、旧世代の並列GCを使用していることも明確に示しています。そして、そのような非並行コレクターの場合、この回答で
Holger

@ホルガー私はこの答えを今日午前中ParalleGCに確認しましたが、それが本当に間違いであることに気づきました。間違いを証明して申し訳ありません(そしてありがとう)。
ユージーン

1
「巨大な割り当て」は正しいヒントです。非並行コレクターでは、古い世代がいっぱいになったときに最初のGCが実行されることを意味するため、十分なスペースの再利用に失敗すると致命的になります。対照的に、配列のサイズを小さくすると、古い世代のメモリが残っているときに若いGCがトリガーされるため、コレクターはオブジェクトを昇格して続行できます。一方、並行コレクターの場合、ヒープが使い果たされる前にgcをトリガーするのが通常です。その-XX:+UseG1GCため-XX:+UseParallelOldGC、新しいJVMで失敗するように、Java 8で動作させるようにします。
ホルガー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.