OutOfMemoryException nonostante l'utilizzo di WeakHashMap


9

In caso contrario System.gc(), il sistema genererà una OutOfMemoryException. Non so perché devo chiamare System.gc()esplicitamente; la JVM dovrebbe chiamarsi gc(), giusto? Si prega di avvisare.

Quanto segue è il mio codice di prova:

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

Come segue, aggiungere -XX:+PrintGCDetailsper stampare le informazioni GC; come vedi, in realtà, la JVM tenta di eseguire una corsa GC completa, ma fallisce; Non conosco ancora il motivo. È molto strano che se rimuovo il commento sulla System.gc();linea, il risultato è positivo:

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

quale versione di jdk? usi qualche parametro -Xms e -Xmx? su quale passo hai ottenuto OOM?
Vladislav Kysliy l'

1
Non riesco a riprodurlo sul mio sistema. In modalità debug vedo che il GC sta facendo il suo lavoro. È possibile verificare in modalità debug se la mappa viene effettivamente cancellata o no?
magicmn

jre 1.8.0_212-b10 -Xmx200m Puoi vedere maggiori dettagli dal registro gc che ho allegato; grazie
Dominic Peng

Risposte:


7

JVM chiamerà GC da solo, ma in questo caso sarà troppo poco, troppo tardi. In questo caso non è solo GC a essere responsabile della cancellazione della memoria. I valori della mappa sono fortemente raggiungibili e vengono cancellati dalla mappa stessa quando vengono invocate determinate operazioni su di essa.

Ecco l'output se attivi gli eventi 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 non viene attivato fino all'ultimo tentativo di inserire valore nella mappa.

WeakHashMap non può cancellare le voci obsolete finché non si verificano le chiavi della mappa su una coda di riferimento. E le chiavi della mappa non si verificano su una coda di riferimento fino a quando non vengono raccolte. L'allocazione di memoria per il nuovo valore della mappa viene attivata prima che la mappa abbia la possibilità di cancellarsi. Quando l'allocazione della memoria ha esito negativo e attiva GC, le chiavi della mappa vengono raccolte. Ma è troppo tardi, non è stata liberata memoria sufficiente per allocare il nuovo valore della mappa. Se riduci il payload, probabilmente finirai con memoria sufficiente per allocare il nuovo valore della mappa e le voci obsolete verranno rimosse.

Un'altra soluzione potrebbe essere il wrapping dei valori stessi in WeakReference. Ciò consentirà a GC di eliminare le risorse senza attendere che la mappa esegua da sola. Ecco l'output:

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)

Molto meglio.


Grazie per la tua risposta, sembra che la tua conclusione sia corretta; mentre provo a ridurre il carico utile da 1024 * 10000 a 1024 * 1000; il codice può funzionare bene; ma ANCORA non capisco molto bene la tua spiegazione; come tuo significato, se hai bisogno di liberare spazio da WeakHashMap, dovresti fare almeno due volte gc; il primo tempo è quello di raccogliere le chiavi dalla mappa e aggiungerle nella coda di riferimento; la seconda volta è raccogliere valori? ma dal primo log che hai fornito, in realtà, JVM aveva già preso due volte il full gc;
Dominic Peng

Stai dicendo che "I valori della mappa sono fortemente raggiungibili e vengono cancellati dalla mappa stessa quando vengono invocate determinate operazioni su di essa". Da dove sono raggiungibili?
Andronico

1
Non sarà sufficiente avere solo due corse GC nel tuo caso. Per prima cosa hai bisogno di una corsa GC, è corretto. Ma il passaggio successivo richiederà una certa interazione con la mappa stessa. Quello che dovresti cercare è il metodo java.util.WeakHashMap.expungeStaleEntriesche legge la coda di riferimento e rimuove le voci dalla mappa rendendo i valori irraggiungibili e soggetti alla raccolta. Solo dopo ciò il secondo passaggio di GC libererà un po 'di memoria. expungeStaleEntriesviene chiamato in un numero di casi come get / put / size o praticamente tutto ciò che di solito fai con una mappa. Questo è il trucco.
tentacolo

1
@Andronicus, questa è di gran lunga la parte più confusa di WeakHashMap. È stato coperto più volte. stackoverflow.com/questions/5511279/…
tentacolo

2
@Andronico questa risposta , in particolare la seconda metà, potrebbe anche essere utile. Anche questo D&R ...
Holger,

5

L'altra risposta è davvero corretta, ho modificato la mia. Come un piccolo addendum, G1GCnon mostrerà questo comportamento, a differenza ParallelGC; che è l'impostazione predefinita in java-8.

Cosa pensi che succederà se cambio leggermente il tuo programma in (esegui jdk-8con -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
    }
}

Funzionerà bene. Perché? Perché dà al tuo programma lo spazio sufficiente per far avvenire nuove allocazioni, prima di WeakHashMapcancellare le sue voci. E l'altra risposta spiega già come ciò accada.

Ora, dentro G1GC, le cose sarebbero un po 'diverse. Quando viene allocato un oggetto così grande (di solito più di 1/2 MB ), questo viene chiamato a humongous allocation. Quando ciò accade, verrà attivato un GC simultaneo . Come parte di quel ciclo: verrà avviata una giovane raccolta e Cleanup phaseverrà avviata una che si occuperà di pubblicare l'evento sul ReferenceQueue, in modo da WeakHashMapcancellarne le voci.

Quindi per questo codice:

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

che eseguo con jdk-13 (dove G1GCè l'impostazione predefinita)

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

Ecco una parte dei registri:

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

Questo fa già qualcosa di diverso. Inizia un concurrent cycle(fatto mentre l'applicazione è in esecuzione), perché c'era un G1 Humongous Allocation. Come parte di questo ciclo simultaneo esegue un giovane ciclo GC (che interrompe l'applicazione durante l'esecuzione)

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

Come parte di quel giovane GC, cancella anche regioni gigantesche , ecco il difetto .


Ora puoi vedere che jdk-13non aspetta che i rifiuti si accumulino nella vecchia regione quando vengono allocati oggetti veramente grandi, ma innesca un ciclo GC simultaneo , che ha salvato la giornata; a differenza di jdk-8.

Potresti voler leggere cosa DisableExplicitGCe / o ExplicitGCInvokesConcurrentsignificare, accoppiato System.gce capire perché chiamare in System.gcrealtà aiuta qui.


1
Java 8 non utilizza G1GC per impostazione predefinita. E i registri GC dell'OP mostrano anche chiaramente che sta usando GC paralleli per la vecchia generazione. E per un collezionista non concorrente, è semplice come descritto in questa risposta
Holger

@Holger stavo rivedendo questa risposta oggi al mattino solo per rendermi conto che è davvero ParalleGC, ho modificato e mi dispiace (e grazie) per avermi dimostrato di sbagliarmi.
Eugene

1
L '"allocazione enorme" è ancora un suggerimento corretto. Con un raccoglitore non concorrente, implica che il primo GC verrà eseguito quando la vecchia generazione è piena, quindi l'incapacità di recuperare abbastanza spazio lo renderà fatale. Al contrario, quando si riducono le dimensioni dell'array, verrà attivato un giovane GC quando è rimasta ancora memoria nella vecchia generazione, in modo che il raccoglitore possa promuovere oggetti e continuare. Per un collezionista simultaneo, d'altra parte, è normale attivare gc prima che l'heap sia esaurito, quindi -XX:+UseG1GCfallo funzionare in Java 8, proprio come -XX:+UseParallelOldGCfa fallire nelle nuove JVM.
Holger
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.