Si no llama System.gc()
, el sistema lanzará una excepción OutOfMemoryException. No sé por qué necesito llamar System.gc()
explícitamente; la JVM debería llamarse a gc()
sí misma, ¿verdad? Por favor avise.
El siguiente es mi código de prueba:
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();
}
}
De la siguiente manera, agregue -XX:+PrintGCDetails
para imprimir la información del GC; como ve, en realidad, la JVM intenta hacer una ejecución completa de GC, pero falla; Yo todavía no sé la razón. Es muy extraño que si descomento la System.gc();
línea, el resultado es 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
fuente
fuente
Respuestas:
JVM llamará a GC por sí mismo, pero en este caso será demasiado poco y demasiado tarde. No es solo GC el responsable de borrar la memoria en este caso. Los valores del mapa son muy accesibles y el mapa los borra cuando se invocan ciertas operaciones en él.
Aquí está la salida si activa los eventos de GC (XX: + PrintGC):
GC no se activa hasta el último intento de poner valor en el mapa.
WeakHashMap no puede borrar las entradas obsoletas hasta que se produzcan claves de mapa en una cola de referencia. Y las claves del mapa no aparecen en una cola de referencia hasta que se recolectan basura. La asignación de memoria para el nuevo valor del mapa se activa antes de que el mapa tenga alguna posibilidad de borrarse. Cuando la asignación de memoria falla y desencadena GC, las claves del mapa se recopilan. Pero es demasiado poco, demasiado tarde: no se ha liberado suficiente memoria para asignar un nuevo valor de mapa. Si reduce la carga útil, probablemente termine con suficiente memoria para asignar un nuevo valor de mapa y se eliminarán las entradas obsoletas.
Otra solución podría ser envolver los valores en WeakReference. Esto permitirá que GC elimine recursos sin esperar que el mapa lo haga por sí mismo. Aquí está la salida:
Mucho mejor.
fuente
java.util.WeakHashMap.expungeStaleEntries
que lea la cola de referencia y elimine las entradas del mapa, lo que hace que los valores sean inalcanzables y estén sujetos a recopilación. Solo después de eso, el segundo paso de GC liberará algo de memoria.expungeStaleEntries
se llama en varios casos como get / put / size o casi todo lo que normalmente haces con un mapa. Esa es la trampa.La otra respuesta es correcta, yo edité la mía. Como una pequeña adición,
G1GC
no exhibirá este comportamiento, a diferencia deParallelGC
; cuál es el valor predeterminado enjava-8
.¿Qué crees que pasará si cambio ligeramente tu programa a (ejecutar
jdk-8
con-Xmx20m
)Funcionará bien. ¿Porqué es eso? Debido a que le da a su programa suficiente espacio para respirar para que ocurran nuevas asignaciones, antes de
WeakHashMap
borrar sus entradas. Y la otra respuesta ya explica cómo sucede eso.Ahora, en
G1GC
, las cosas serían un poco diferentes. Cuando se asigna un objeto tan grande (más de 1/2 a MB por lo general ), esto se llamaría ahumongous allocation
. Cuando eso sucede , se activará un GC concurrente . Como parte de ese ciclo: se activará una colección joven yCleanup phase
se iniciará una que se encargará de publicar el evento en elReferenceQueue
, para queWeakHashMap
borre sus entradas.Entonces para este código:
que ejecuto con jdk-13 (donde
G1GC
es el valor predeterminado)Aquí hay una parte de los registros:
Esto ya hace algo diferente. Comienza un
concurrent cycle
(hecho mientras su aplicación se está ejecutando), porque había unG1 Humongous Allocation
. Como parte de este ciclo concurrente, realiza un ciclo GC joven (que detiene la aplicación mientras se ejecuta)Como parte de ese GC joven , también borra regiones gigantescas , aquí está el defecto .
Ahora puede ver que
jdk-13
no espera que se acumule basura en la región anterior cuando se asignan objetos realmente grandes, sino que desencadena un ciclo de GC concurrente , que salvó el día; a diferencia de jdk-8.Es posible que desee leer qué
DisableExplicitGC
y / oExplicitGCInvokesConcurrent
significado, junto conSystem.gc
y comprender por qué llamarSystem.gc
realmente ayuda aquí.fuente
ParalleGC
, he editado y lo siento (y gracias) por demostrarme que estoy equivocado.-XX:+UseG1GC
que debe funcionar en Java 8, al igual que-XX:+UseParallelOldGC
hace que falle en las nuevas JVM.