A medida que nuestras aplicaciones entran en proceso de ejecución se comienzan a crear objetos como resultado de la ejecución de muchas de las instrucciones que colocamos. Estos objetos comienzan a ocupar la memoria heap de la máquina virtual. El proceso de ir borrando los objetos viejos y que no se usen se lleva a cabo por una parte esencial de la JVM: el Garbage Collector o recolector de basura.

¿Qué es el Garbage Collector?

La recolección de basura es un mecanismo implícito de algunos lenguajes de programación interpretados o semi-interpretados encargado de la gestión de memoria.

Representación de la memoria. Tomada de Wikipedia.

En lenguajes como C++ el programador es el encargado de crear destructores y eliminar los propios objetos que va creando, como mismo existe un constructor para crear instancias, existe un destructor para eliminarlas.

La mayoría de los lenguajes han liberado a los programadores de esta tarea e implementan su propio recolector de basura (o más de uno como en el caso de la JVM).

¿Cómo funciona internamente la gestión de la memoria al crear objetos?

En este otro artículo, describíamos la arquitectura de la JVM, hablamos de la memoria heap donde se alojan los objetos en tiempo de ejecución, veamos más en detalle como funciona este proceso. La información la hemos obtenido basándonos en la web oficial de Oracle.

La memoria se divide en tres partes: Young Generation, Old Generation y Permanent Generation.

Imagen oficial tomada de la web de Oracle

La zona de Young Generation, se divide en Eden, que almacena los objetos que hemos acado de crear, y en un espacio de supervivencia (zona naranja). Cuando el recolector de basura pasa elimina todos aquellos objetos que no tienen referencia a los que se encuentran en la zona Eden y los supervivientes los pasa al espacio de supervivencia.

En la próxima pasada del GC sucede lo mismo para el espacio Eden. Los objetos sin referencia se eliminan y los objetos con referencia se mueven a un espacio de supervivencia. Sin embargo, en este caso, se mueven al segundo espacio de sobreviviente (S1). Además, los objetos del último GC menor en el primer espacio de sobreviviente (S0) tienen su edad incrementada y se mueven a S1. Una vez que todos los objetos supervivientes se han movido a S1, tanto S0 como eden se borran. Tengamos en cuenta que ahora tenemos un objeto envejecido de manera diferente en el espacio del sobreviviente.

En la siguiente pasada, se repite el mismo proceso. Sin embargo, esta vez los espacios de sobrevivientes cambian. Los objetos referenciados se mueven a S0. Los objetos supervivientes están envejecidos. Eden (que se paso a S1) y S1 se borran.

Después de varias pasadas, si aún hay objetos vivos (con referencias en uso), se pasan a Old Generation. Las revisiones del GC sobre Old Generation y Permanent serán mas espaciadas, en algunas implementaciones cuando el heap este prácticamente lleno.

Clasificación de los recolector de basura

Los recolectores de basura se pueden clasificar en base a su comportamiento en diferentes categorías, según el equipo de Autentia se pueden clasificar como sigue a continuación.

Concurrente vs Stop-The-World (STW)

Concurrentes son aquellos que pueden realizar su trabajo a la vez que se ejecuta una aplicación.

STW son aquellos que paran la ejecución de la aplicación para realizar sus tareas de limpieza.

Paralelo vs Serie

Paralelos son aquellos que pueden aprovechar la existencia de múltiples CPUs para realizar su trabajo.

Serie son aquellos que se ejecutan en un único hilo.

Incremental vs Monolítico

Incrementales son aquellos dividen su trabajo en diferentes fases u operaciones, siendo estas fases separadas en el tiempo.

Monolíticos son aquellos que realizan su trabajo en una única tarea.

Precisos vs Conservadores:

Precisos son aquellos que pueden identificar y procesar todas las referencias de los objetos en el momento de la recolección.

Conservadores son aquellos que desconocen algunas de las referencias de los objetos en el momento de la recolección, o que no están seguros de si un campo es una referencia o no.

Compactadores vs No Compactadores

Compactadores son aquellos que, tras realizar las tareas de limpieza mueven los objetos, actualizando sus referencias, minimizando de esta manera la fragmentación de la memoria. Esta operación implica ser STW.

No compactadores son aquellos que se limitan a eliminar la basura de la memoria.

Control de la memoria Heap

Como hemos visto, todo este proceso de creación y destrucción de objetos se lleva a cabo en el heap de la JVM. Java nos ofrece mecanismos para configurar ciertos parámetros de esta memoria, que podemos establecer como parámetros a nuestra aplicación Java.

  • -Xms: Establece el tamaño inicial del heap.
  • -Xmx: Establece el tamaño máximo del heap.
  • -Xmn: Establece el tamaño del espacio del heap para objetos de generación jóven.
  • -XX:SurvivorRatio=: Establece el tamaño del espacio de supervivientes. Establece el ratio de tamaño entre cada espacio de supervivientes y Eden.

El heap podemos monitorizarlo e incluso definir alertas cuando se den algunas situaciones, la lista de parámetros debajo permite el monitoreo de la memoria heap.

  • -XX:HeapDumpPath=./java_pid.hprof: Establece la ruta donde se generará el dump del heap.
  • -XX:+HeapDumpOnOutOfMemoryError: Establece que se haga un dump del heap cuando se produzca un OutOfMemoryException.
  • -XX:OnOutOfMemoryError=”;”: Lanza comandos definidos por el usuario cuando se produzca un OutOfMemoryException.

Independiente a estos comandos, recomiendo la herramienta VisualVM, una solución de código abierto muy poderosa para monitorear como “trata” nuestra aplicación a la JVM.

Seleccionando el Garbage Collector para la limpieza

  • -XX:+UseSerialGC: Recolector Serie.
  • -XX:+UseParallelGC: Recolector Paralelo
  • -XX:ParallelGCThreads=: Establecer el número de hilos para ejecutar el GC.
  • -XX:+UseParallelOldGC: Recolector Paralelo con compactación.
  • -XX:+UseConcMarkSweepGC: Recolector CMS.
  • XX:ParallelCMSThreads=: Establecer el número de hilos para ejecutar el GC.
  • -XX:+UseG1GC: Recolector G1.

Por experiencia propia, que hemos tenido que ejecutar programas Java en computadores de bajos recursos, hemos empleado con buenos resultados el recolector G1, unido a G1 hemos establecido en microservicios Java el heap máximo (Xmx) a 80MB de RAM y los servicios han trabajado con buena calidad de servicio y rendiemiento, en pruebas de cargas realizadas hemos visualizado con VisualVM; que cuando la memoria Heap establecida como límite esta por llenarse el recolector G1 hace su trabajo con buenos resultados. Ten en cuenta que si G1 tiene que trabajar mucho esto impactará en el procesamiento (uso de CPU intensivo).

Esperamos te sea útil este artículo. Déjanos tus comentarios e invítanos una cerveza.

Leave a Reply

Your email address will not be published. Required fields are marked *