OutOfMemoryException apesar de usar WeakHashMap


9

Se não ligar System.gc(), o sistema lançará uma OutOfMemoryException. Não sei por que preciso ligar System.gc()explicitamente; a JVM deve se chamar gc(), certo? Por favor informar.

A seguir está o meu código de teste:

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

A seguir, adicione -XX:+PrintGCDetailspara imprimir as informações do GC; como você vê, na verdade, a JVM tenta executar uma execução completa do GC, mas falha; Eu ainda não sei o motivo. É muito estranho que, se eu descomentar a System.gc();linha, o resultado é 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

qual versão do jdk? você usa algum parâmetro -Xms e -Xmx? em qual etapa você obteve OOM?
— Vladislav Kysliy

11
Não consigo reproduzir isso no meu sistema. No modo de depuração, posso ver que o GC está fazendo seu trabalho. Você pode verificar no modo de depuração se o Mapa está realmente sendo limpo ou não?
— magicmn

jre 1.8.0_212-b10 -Xmx200m Você pode ver mais detalhes do log do gc que eu anexei; thx
— Dominic Peng

Respostas:


7

A JVM ligará para a GC por conta própria, mas, neste caso, será tarde demais. Não é apenas a GC que é responsável por limpar a memória neste caso. Os valores do mapa são fortemente alcançáveis ​​e são limpos pelo próprio mapa quando determinadas operações são invocadas nele.

Aqui está a saída se você ativar eventos do 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

O GC não é acionado até a última tentativa de colocar valor no mapa.

O WeakHashMap não pode limpar entradas obsoletas até que as chaves do mapa ocorram em uma fila de referência. E as chaves do mapa não ocorrem em uma fila de referência até serem coletadas como lixo. A alocação de memória para o novo valor do mapa é acionada antes que o mapa tenha alguma chance de se limpar. Quando a alocação de memória falha e aciona o GC, as chaves do mapa são coletadas. Mas é tarde demais - não foi liberada memória suficiente para alocar novo valor do mapa. Se você reduzir a carga útil, provavelmente terá memória suficiente para alocar novo valor do mapa e as entradas obsoletas serão removidas.

Outra solução poderia ser agrupar os valores em WeakReference. Isso permitirá que o GC limpe os recursos sem esperar que o mapa faça isso por conta própria. Aqui está a saída:

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)

Muito melhor.


Thx pela sua resposta, parece que sua conclusão está correta; enquanto eu tento reduzir a carga útil de 1024 * 10000 para 1024 * 1000; o código pode funcionar bem; mas ainda não entendo muito sua explicação; como seu significado, se precisar liberar espaço do WeakHashMap, faça gc duas vezes pelo menos; o primeiro momento é coletar chaves do mapa e adicioná-las à fila de referência; a segunda vez é coletar valores? mas, a partir do primeiro registro que você forneceu, na verdade, a JVM já recebeu o gc completo duas vezes;
— Dominic Peng

Você está dizendo que "os valores do mapa são fortemente alcançáveis ​​e são limpos pelo próprio mapa quando determinadas operações são invocadas nele". De onde eles são acessíveis?
— Andronicus

11
Não será suficiente ter apenas duas execuções de GC no seu caso. Primeiro, você precisa de uma execução de GC, isso está correto. Mas o próximo passo exigirá alguma interação com o próprio mapa. O que você deve procurar é um método java.util.WeakHashMap.expungeStaleEntriesque leia a fila de referência e remova as entradas do mapa, tornando os valores inacessíveis e sujeitos à coleção. Somente depois disso, a segunda passagem do GC liberará alguma memória. expungeStaleEntriesé chamado em vários casos, como get / put / size ou praticamente tudo o que você costuma fazer com um mapa. Essa é a pegadinha.
— tentacle

11
@ Andrronicus, esta é de longe a parte mais confusa do WeakHashMap. Foi coberto várias vezes. stackoverflow.com/questions/5511279/…
— tentacle

2
@ Andrronicus, esta resposta , especialmente a segunda metade, também pode ser útil. Também este Q & A ...
— Holger

5

A outra resposta está realmente correta, editei a minha. Como um pequeno adendo, G1GCnão exibirá esse comportamento, ao contrário ParallelGC; qual é o padrão em java-8.

O que você acha que vai acontecer se eu mudar um pouco o seu programa (RUN sob jdk-8com -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
    }
}

Funcionará muito bem. Por que é que? Como ele oferece ao seu programa espaço suficiente para novas alocações, antes de WeakHashMaplimpar suas entradas. E a outra resposta já explica como isso acontece.

Agora, as G1GCcoisas seriam um pouco diferentes. Quando um objeto tão grande é alocado ( geralmente mais de 1/2 a MB ), isso seria chamado de humongous allocation. Quando isso acontece, um GC simultâneo será acionado. Como parte desse ciclo: uma coleção jovem será acionada e Cleanup phaseserá iniciado um que cuidará da publicação do evento no ReferenceQueue, para que WeakHashMapapague suas entradas.

Então, para este código:

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

que eu corro com jdk-13 (onde G1GCé o padrão)

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

Aqui está uma parte dos logs:

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

Isso já faz algo diferente. Ele inicia um concurrent cycle(feito enquanto o aplicativo está sendo executado), porque havia um G1 Humongous Allocation. Como parte desse ciclo simultâneo, ele executa um ciclo de GC jovem (que interrompe seu aplicativo durante a execução)

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

Como parte desse jovem CG, ele também limpa regiões enormes , eis o defeito .


Agora você pode ver que jdk-13não espera o lixo se acumular na região antiga quando objetos realmente grandes são alocados, mas dispara um ciclo simultâneo de GC, que salvou o dia; ao contrário do jdk-8.

Você pode ler o que DisableExplicitGCe / ou o que ExplicitGCInvokesConcurrentsignifica, juntamente com System.gce entender por que a chamada System.gcrealmente ajuda aqui.


11
O Java 8 não usa G1GC por padrão. E os logs do GC do OP também mostram claramente que ele está usando o GC paralelo para a geração antiga. E para um coletor não concorrente, é tão simples como descrito nesta resposta
— Holger

@ Holger Eu estava revendo esta resposta hoje de manhã apenas para perceber que é verdade ParalleGC. Eu editei e desculpe (e obrigado) por provar que estou errado.
— Eugene

11
A "alocação imensa" ainda é uma dica correta. Com um coletor não simultâneo, isso implica que o primeiro GC será executado quando a geração antiga estiver cheia; portanto, a falha na recuperação de espaço suficiente a tornará fatal. Por outro lado, quando você reduz o tamanho da matriz, um GC jovem é acionado quando ainda resta memória na geração antiga, para que o coletor possa promover objetos e continuar. Para um coletor simultâneo, por outro lado, é normal disparar o gc antes que o heap esteja esgotado; portanto, -XX:+UseG1GCfaça-o funcionar no Java 8, assim como -XX:+UseParallelOldGCfalha nas novas JVMs.
— Holger
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.