Guía de System.gc ()

1. Información general

En este tutorial, vamos a investigar el método System.gc () ubicado en el paquete java.lang .

Llamar explícitamente a System.gc () es conocido por ser una mala práctica. Intentemos entender por qué y si hay casos de uso cuando llamar a este método podría ser útil.

2. Recolección de basura

La máquina virtual Java decide realizar la recolección de basura cuando hay indicaciones para hacerlo. Esas indicaciones difieren de una implementación de GC a otra. Se basan en diferentes heurísticas. Sin embargo, hay algunos momentos en los que GC se ejecutará con seguridad:

  • La nueva generación (espacio titular) está llena, lo que desencadena GC menor
  • La vieja generación (espacios Eden + Survivor0 + Survivor1) está llena, lo que activa la GC mayor / completa

Lo único que es independiente de la implementación de GC es la elegibilidad del objeto para la recolección de basura.

Ahora, veremos el método System.gc () en sí.

3. System.gc ()

Una invocación del método es simple:

System.gc()

La documentación oficial de Oracle establece que:

Llamar al método gc sugiere que la máquina virtual Java se esfuerza por reciclar los objetos no utilizados para que la memoria que ocupan actualmente esté disponible para su reutilización rápida.

No hay garantía de que se active el GC real .

System.gc () desencadena un GC importante. Por lo tanto, existe el riesgo de pasar algún tiempo en la fase de detener el mundo, según la implementación de su recolector de basura. Como resultado, tenemos una herramienta poco confiable con una penalización de rendimiento potencialmente significativa .

La existencia de una invocación explícita a la recolección de basura debería ser una señal de alerta seria para todos.

Podemos evitar que System.gc () haga cualquier trabajo usando el indicador -XX: DisableExplicitGC JVM.

3.1. La optimización del rendimiento

Vale la pena señalar que justo antes de lanzar un OutOfMemoryError, la JVM realizará un GC completo. Por lo tanto, una llamada explícita a System.gc () no nos salvará de fallar .

Los recolectores de basura hoy en día son realmente inteligentes. Tienen todo el conocimiento sobre el uso de la memoria y otras estadísticas para poder tomar decisiones adecuadas. Por tanto, debemos confiar en ellos.

En caso de problemas de memoria, tenemos un montón de configuraciones que podemos cambiar para ajustar nuestra aplicación, comenzando por elegir un recolector de basura diferente, estableciendo el tiempo de aplicación deseado / relación de tiempo de GC y, finalmente, terminando con la configuración de tamaños fijos para segmentos de memoria.

También hay formas de mitigar los efectos de Full GC causados ​​por una llamada explícita. Podemos usar una de las banderas:

-XX:+ExplicitGCInvokesConcurrent

o:

-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

Si realmente queremos que nuestra aplicación funcione correctamente, deberíamos resolver el verdadero problema de memoria subyacente.

En el próximo capítulo, veremos un ejemplo práctico en el que llamar explícitamente a System.gc () parece ser útil.

4. Ejemplo de uso

4.1. Guión

Escribamos una aplicación de prueba. Queremos encontrar una situación en la que llamar a System.gc () pueda ser útil .

La recolección de basura menor ocurre con más frecuencia que la principal. Entonces, probablemente deberíamos enfocarnos en lo último. Un solo objeto se mueve a un espacio de tenencia si “sobrevivió” a algunas colecciones y aún es accesible desde las raíces de GC.

Imaginemos que tenemos una enorme colección de objetos que están vivos desde hace algún tiempo. Luego, en algún momento, vamos a borrar la colección de objetos. ¿Quizás es un buen momento para ejecutar System.gc () ?

4.2. Aplicación de demostración

Crearemos una aplicación de consola simple que nos permitirá simular ese escenario:

public class DemoApplication { private static final Map cache = new HashMap(); public static void main(String[] args) { Scanner scanner = new Scanner(System.in); while (scanner.hasNext()) { final String next = scanner.next(); if ("fill".equals(next)) { for (int i = 0; i < 1000000; i++) { cache.put(randomUUID().toString(), randomUUID().toString()); } } else if ("invalidate".equals(next)) { cache.clear(); } else if ("gc".equals(next)) { System.gc(); } else if ("exit".equals(next)) { System.exit(0); } else { System.out.println("unknown"); } } } }

4.3. Ejecutando la demostración

Ejecutemos nuestra aplicación con algunas banderas adicionales:

-XX:+PrintGCDetails -Xloggc:gclog.log -Xms100M -Xmx500M -XX:+UseConcMarkSweepGC

Los dos primeros indicadores son necesarios para registrar la información del GC. Los siguientes dos indicadores establecen el tamaño del montón inicial y luego el tamaño máximo del montón. Queremos mantener el tamaño del montón bajo para forzar a GC a ser más activo. Por último, estamos decidiendo utilizar CMS: recolector de basura Concurrent Mark and Sweep. ¡Es hora de ejecutar nuestra aplicación!

Primero, intentemos llenar el espacio permanente . Tipo de relleno.

Podemos investigar nuestro archivo gclog.log para ver qué sucedió. Veremos alrededor de 15 colecciones. La línea registrada para colecciones individuales se ve así:

197.057: [GC (Allocation Failure) 197.057: [ParNew: 67498K->40K(75840K), 0.0016945 secs] 168754K->101295K(244192K), 0.0017865 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] secs]

Como podemos ver, la memoria está llena.

A continuación, nos dejó la fuerza System.gc () mediante tipificación GC . Podemos ver que el uso de la memoria no cambió significativamente:

238.810: [Full GC (System.gc()) 238.810: [CMS: 101255K->101231K(168352K); 0.2634318 secs] 120693K->101231K(244192K), [Metaspace: 32186K->32186K(1079296K)], 0.2635908 secs] [Times: user=0.27 sys=0.00, real=0.26 secs]

After a few more runs, we'll see that memory size stays at the same level.

Let's clear the cache by typing invalidate. We should see no more log lines appear in the gclog.log file.

We can try to fill cache a few more times, but no GC is happening. This is a moment when we can outsmart the garbage collector. Now, after forcing GC, we'll see a line like:

262.124: [Full GC (System.gc()) 262.124: [CMS: 101523K->14122K(169324K); 0.0975656 secs] 103369K->14122K(245612K), [Metaspace: 32203K->32203K(1079296K)], 0.0977279 secs] [Times: user=0.10 sys=0.00, real=0.10 secs]

We've released an impressive amount of memory! But was it really necessary right now? What happened?

According to this example, calling System.gc() might seem tempting when we're releasing big objects or invalidating caches.

5. Other Usages

There are very few reasons when an explicit call to the System.gc() method might be useful.

One possible reason is cleaning memory after server startup — we're starting a server or application which does a lot of preparation. After that, there are a lot of objects to be finalized. However, cleaning after such preparation shouldn't be our responsibility.

Another is memory leak analysisit's more a debugging practice than something we would like to keep in the production code. Calling System.gc() and seeing heap space still being high might be an indication of a memory leak.

6. Summary

In this article, we investigated the System.gc() method and when it might seem useful.

Nunca debemos confiar en él cuando se trata de la exactitud de nuestra aplicación. En la mayoría de los casos, GC es más inteligente que nosotros y, en caso de problemas de memoria, deberíamos considerar ajustar la máquina virtual en lugar de realizar una llamada tan explícita.

Como de costumbre, el código utilizado en este artículo se puede encontrar en GitHub.