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:+PrintGCDetails
para 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
java
java-8
garbage-collection
out-of-memory
weak-references
Dominic Peng
fonte
fonte
Respostas:
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):
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:
Muito melhor.
fonte
java.util.WeakHashMap.expungeStaleEntries
que 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.A outra resposta está realmente correta, editei a minha. Como um pequeno adendo,
G1GC
não exibirá esse comportamento, ao contrárioParallelGC
; qual é o padrão emjava-8
.O que você acha que vai acontecer se eu mudar um pouco o seu programa (RUN sob
jdk-8
com-Xmx20m
)Funcionará muito bem. Por que é que? Como ele oferece ao seu programa espaço suficiente para novas alocações, antes de
WeakHashMap
limpar suas entradas. E a outra resposta já explica como isso acontece.Agora, as
G1GC
coisas seriam um pouco diferentes. Quando um objeto tão grande é alocado ( geralmente mais de 1/2 a MB ), isso seria chamado dehumongous allocation
. Quando isso acontece, um GC simultâneo será acionado. Como parte desse ciclo: uma coleção jovem será acionada eCleanup phase
será iniciado um que cuidará da publicação do evento noReferenceQueue
, para queWeakHashMap
apague suas entradas.Então, para este código:
que eu corro com jdk-13 (onde
G1GC
é o padrão)Aqui está uma parte dos logs:
Isso já faz algo diferente. Ele inicia um
concurrent cycle
(feito enquanto o aplicativo está sendo executado), porque havia umG1 Humongous Allocation
. Como parte desse ciclo simultâneo, ele executa um ciclo de GC jovem (que interrompe seu aplicativo durante a execução)Como parte desse jovem CG, ele também limpa regiões enormes , eis o defeito .
Agora você pode ver que
jdk-13
nã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
DisableExplicitGC
e / ou o queExplicitGCInvokesConcurrent
significa, juntamente comSystem.gc
e entender por que a chamadaSystem.gc
realmente ajuda aqui.fonte
ParalleGC
. Eu editei e desculpe (e obrigado) por provar que estou errado.-XX:+UseG1GC
faça-o funcionar no Java 8, assim como-XX:+UseParallelOldGC
falha nas novas JVMs.