OutOfMemoryException meskipun menggunakan WeakHashMap

9

Jika tidak menelepon System.gc(), sistem akan mengeluarkan OutOfMemoryException. Saya tidak tahu mengapa saya harus menelepon System.gc()secara eksplisit; JVM harus memanggil gc()dirinya sendiri, bukan? Mohon saran.

Berikut ini adalah kode pengujian saya:

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

Sebagai berikut, tambahkan -XX:+PrintGCDetailsuntuk mencetak info GC; seperti yang Anda lihat, sebenarnya, JVM mencoba untuk menjalankan GC penuh, tetapi gagal; Saya masih belum tahu alasannya. Sangat aneh bahwa jika saya menghapus tanda komentar pada System.gc();baris, hasilnya positif:

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
Dominic Peng
sumber
apa versi jdk? apakah Anda menggunakan parameter -Xms dan -Xmx? pada langkah mana Anda mendapat OOM?
Vladislav Kysliy
1
Saya tidak dapat mereproduksi ini di sistem saya. Dalam mode debug, saya dapat melihat bahwa GC melakukan tugasnya. Bisakah Anda memeriksa dalam mode debug jika Peta benar-benar dihapus atau tidak?
magicmn
jre 1.8.0_212-b10 -Xmx200m Anda dapat melihat lebih detail dari gc log yang saya lampirkan; thx
Dominic Peng

Jawaban:

7

JVM akan memanggil GC sendiri, tetapi dalam hal ini akan terlalu sedikit terlambat. Bukan hanya GC yang bertanggung jawab dalam membersihkan memori dalam kasus ini. Nilai peta sangat terjangkau dan dibersihkan oleh peta itu sendiri ketika operasi tertentu dijalankan di atasnya.

Inilah hasilnya jika Anda menghidupkan acara 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 tidak terpicu hingga upaya terakhir untuk memberi nilai pada peta.

WeakHashMap tidak dapat menghapus entri basi sampai kunci peta terjadi pada antrian referensi. Dan kunci peta tidak muncul pada antrian referensi sampai mereka dikumpulkan. Alokasi memori untuk nilai peta baru dipicu sebelum peta memiliki peluang untuk membersihkan dirinya sendiri. Ketika alokasi memori gagal dan memicu GC, kunci peta dikumpulkan. Tetapi sudah terlalu sedikit terlambat - tidak cukup memori yang dibebaskan untuk mengalokasikan nilai peta baru. Jika Anda mengurangi payload, Anda mungkin berakhir dengan memori yang cukup untuk mengalokasikan nilai peta baru dan entri basi akan dihapus.

Solusi lain bisa dengan membungkus nilai-nilai itu sendiri ke dalam WeakReference. Ini akan memungkinkan GC menghapus sumber daya tanpa menunggu peta melakukannya sendiri. Inilah hasilnya:

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)

Jauh lebih baik.

sungut
sumber
Terima kasih atas jawaban Anda, sepertinya kesimpulan Anda benar; sementara saya mencoba mengurangi payload dari 1024 * 10000 ke 1024 * 1000; kode dapat bekerja dengan baik; tetapi saya MASIH tidak terlalu memahami penjelasan Anda; sebagai makna Anda, jika perlu melepaskan ruang dari WeakHashMap, harus melakukan gc setidaknya dua kali; waktu pertama adalah mengumpulkan kunci dari peta, dan menambahkannya ke dalam antrian referensi; yang kedua adalah mengumpulkan nilai? tetapi dari log pertama yang Anda berikan, sebenarnya, JVM telah mengambil gc penuh dua kali;
Dominic Peng
Anda mengatakan, bahwa "Nilai peta sangat dapat dijangkau dan dihapus oleh peta itu sendiri ketika operasi tertentu diminta.". Dari mana mereka bisa dijangkau?
Andronicus
1
Tidak cukup hanya dengan memiliki dua proses GC dalam kasus Anda. Pertama, Anda perlu satu kali menjalankan GC, itu benar. Tetapi langkah selanjutnya akan membutuhkan beberapa interaksi dengan peta itu sendiri. Apa yang harus Anda cari adalah metode java.util.WeakHashMap.expungeStaleEntriesyang membaca antrian referensi dan menghapus entri dari peta sehingga membuat nilai tidak terjangkau dan tunduk pada koleksi. Hanya setelah itu dilakukan pass kedua GC akan membebaskan memori. expungeStaleEntriesdisebut dalam sejumlah kasus seperti get / put / size atau hampir semua yang biasanya Anda lakukan dengan peta. Itu tangkapannya.
tentakel
1
@Andronicus, ini adalah bagian yang paling membingungkan dari WeakHashMap. Itu dibahas beberapa kali. stackoverflow.com/questions/5511279/...
tentacle
2
@Andronicus jawaban ini , terutama paruh kedua, mungkin bisa membantu juga. Juga T&J ini ...
Holger
5

Jawaban lainnya memang benar, saya mengedit milik saya. Sebagai tambahan kecil, G1GCtidak akan menunjukkan perilaku ini, tidak seperti ParallelGC; yang merupakan default di bawah java-8.

Menurut Anda apa yang akan terjadi jika saya sedikit mengubah program Anda (berjalan jdk-8dengan -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
    }
}

Ini akan bekerja dengan baik. Mengapa demikian? Karena itu memberi program Anda ruang bernapas yang cukup untuk alokasi baru terjadi, sebelum WeakHashMapmembersihkan entri. Dan jawaban lain sudah menjelaskan bagaimana itu terjadi.

Sekarang, dalam G1GC, segalanya akan sedikit berbeda. Ketika seperti benda besar dialokasikan (lebih dari 1/2 MB biasanya ), ini akan disebut humongous allocation. Ketika itu terjadi, GC bersamaan akan dipicu. Sebagai bagian dari siklus itu: koleksi muda akan dipicu dan Cleanup phaseakan diinisiasi yang akan mengurus posting acara ke ReferenceQueue, sehingga WeakHashMapmembersihkan entri.

Jadi untuk kode ini:

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
    }
}

yang saya jalankan dengan jdk-13 (di mana G1GCdefault)

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

Berikut adalah sebagian dari log:

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

Ini sudah melakukan sesuatu yang berbeda. Itu dimulai concurrent cycle(dilakukan saat aplikasi Anda sedang berjalan), karena ada a G1 Humongous Allocation. Sebagai bagian dari siklus bersamaan ini, ia melakukan siklus GC muda (yang menghentikan aplikasi Anda saat menjalankan)

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

Sebagai bagian dari GC muda itu, ia juga membersihkan daerah-daerah raksasa , di sini adalah cacatnya .


Anda sekarang dapat melihat bahwa jdk-13tidak menunggu sampah menumpuk di wilayah lama ketika benda yang sangat besar dialokasikan, tetapi memicu siklus GC bersamaan , yang menyelamatkan hari; tidak seperti jdk-8.

Anda mungkin ingin membaca apa DisableExplicitGCdan / atau apa ExplicitGCInvokesConcurrentmaksudnya, digabungkan dengan System.gcdan memahami mengapa menelepon System.gcsebenarnya membantu di sini.

Eugene
sumber
1
Java 8 tidak menggunakan G1GC secara default. Dan log GC OP juga jelas menunjukkan bahwa ia menggunakan GC paralel untuk generasi lama. Dan untuk pengumpul yang tidak serentak, sesederhana yang dijelaskan dalam jawaban ini
Holger
@ Holger Saya meninjau jawaban ini hari ini di pagi hari hanya untuk menyadari bahwa itu memang ParalleGC, saya telah mengedit dan minta maaf (dan terima kasih) karena membuktikan saya salah.
Eugene
1
"Alokasi besar" masih merupakan petunjuk yang benar. Dengan kolektor non-konkuren, ini menyiratkan bahwa GC pertama akan berjalan ketika generasi lama penuh, sehingga kegagalan untuk mendapatkan kembali ruang yang cukup akan membuatnya fatal. Sebaliknya, ketika Anda mengurangi ukuran array, GC muda akan dipicu ketika masih ada memori di generasi lama yang tersisa, sehingga kolektor dapat mempromosikan objek dan melanjutkan. Untuk pengumpul bersamaan, di sisi lain, itu normal untuk memicu gc sebelum heap habis, jadi -XX:+UseG1GCbuat itu berfungsi di Java 8, sama seperti -XX:+UseParallelOldGCmembuatnya gagal di JVM baru.
Holger