Comprenda cómo funciona Java Garbage Collection y optimice el uso de memoria en sus aplicaciones. Conozca las complejidades de la gestión de la memoria en Java.
La recolección de basura es un proceso crucial que puede ayudar a cualquier empresa de desarrollo de Java. Esta poderosa característica de los lenguajes de programación gestiona hábilmente la asignación y desasignación de memoria, evitando pérdidas de memoria y optimizando la utilización de recursos. Actuando como un cuidador constante, limpia diligentemente los objetos no utilizados, evitando que nos inunde un desorden digital innecesario. Como empresa de desarrollo de Java, a menudo nos encontramos con este desafío en nuestros esfuerzos de codificación, y la recolección de basura proporciona una solución elegante para ello. Profundicemos en este sofisticado mecanismo.
GC, el héroe anónimo de la programación Java, no sólo nos limpia sino que también hace que la memoria Java sea eficiente. Es crucial porque nosotros, como programadores, necesitamos administrar bien la memoria, liberando recursos, especialmente objetos no referenciados, y a menudo lo olvidamos en nuestra búsqueda de nuevas ideas (y a veces por pereza).
Cómo funciona la recolección de basura de Java
Veamos los detalles de cómo funciona este limpiador silencioso en Java.
La recolección de basura en Java es un proceso que administra automáticamente la memoria recuperando objetos no utilizados que ya no están en uso.
Estructura de la memoria
En Java, la memoria se divide en memoria de pila, memoria de montón y metaespacio. Miremos más de cerca.
memoria de pila
La memoria de pila es una región de la memoria que almacena variables locales, referencias de objetos , parámetros de métodos y otros datos específicos del método durante la ejecución de un método. El tamaño de la memoria de la pila es fijo.
memoria de pila
La memoria de montón es una región de la memoria que se utiliza para almacenar los objetos reales. Su tamaño es fijo y puede aumentar o disminuir dinámicamente según sea necesario.
Aquí hay un ejemplo.
Número entero = nuevo entero (12);
Esto crea una variable "num" en la memoria de la pila y un nuevo objeto entero en la memoria del montón. La variable "num" en la memoria de la pila almacena una referencia al objeto original.
Metaespacio
Metaspace es una parte de la memoria nativa utilizada por la Máquina Virtual Java (JVM) para almacenar metadatos sobre clases y métodos estáticos. Reemplazó la memoria del montón de Generación Permanente, que era parte de la memoria del montón.
En versiones anteriores de Java, PermGen se utilizaba para almacenar metadatos sobre clases y métodos estáticos. Pero tenía algunas limitaciones. Uno de ellos fue el hecho de que tenían un tamaño fijo. Otro problema fue que el espacio PermGen se recolectaba como basura junto con el resto del montón, lo que generaba problemas de rendimiento.
El tamaño del metaespacio se puede cambiar dinámicamente y se recolecta basura por separado. Permite compartir metadatos de clases entre múltiples instancias de JVM, lo que puede reducir el uso de memoria y mejorar el rendimiento.
Elegibilidad para la recolección de basura.
Los objetos activos accesibles son aquellos a los que alguna parte del programa hace referencia, mientras que los objetos muertos o inaccesibles son aquellos a los que ninguna parte del programa hace referencia. Por ejemplo:
Número entero = nuevo entero (12);
número = nulo;
Como se mencionó, la primera línea crea un nuevo objeto Integer en la memoria del montón y una variable en la memoria de la pila, que almacena una referencia al objeto original.
Entonces, en la siguiente línea, cambiaremos la referencia de "num", lo que significa que "num" no se refiere al objeto Integer que creamos anteriormente. De hecho, ninguna parte de nuestro programa hace referencia a este objeto Integer. Por tanto, es un objeto inaccesible o muerto. Los objetos muertos se pueden recoger como basura.
Los objetos se vuelven inaccesibles cuando:
- Todas las variables que hacen referencia a este objeto ya no hacen referencia a él (se establecen en nulo o en un valor diferente).
- Los objetos creados dentro de un método serán inaccesibles cuando ese método se libere de la memoria de la pila.
Islas de aislamiento
Una isla de aislamiento se refiere a un grupo de objetos que hacen referencia entre sí pero que ya no hacen referencia a ningún objeto en el programa. En el siguiente ejemplo, "a" y "b" hacen referencia entre sí, pero ningún otro objeto hace referencia a ellos.
clase Nodo { Nodo siguiente; Nodo anterior; } Nodo a = nuevo Nodo; Nodo b = nuevo Nodo; a.siguiente = b; b.anterior = a;
Para romper la isla de aislamiento, necesitamos cambiar las referencias a los objetos. Aquí, se pueden recolectar basura solo después de cambiar las referencias de "a" y "b" (por ejemplo, establecer a y b en nulos).
Partes de la memoria del montón
Como se mencionó anteriormente, la memoria dinámica es la parte de la memoria responsable de almacenar objetos. Está dividido en el Espacio de la Generación Joven y el Espacio de la Vieja Generación.
Generación joven
En Java, la memoria dinámica de la generación más joven es donde se crean nuevos objetos. Este segmento de memoria se divide en dos secciones: el espacio Eden y los espacios Survivor.
Espacio Edén
El espacio Edén es la parte del espacio generacional joven donde se asignan nuevos objetos.
Espacios de supervivencia
Los objetos en el espacio del Edén que sobreviven a una ronda de recolección de basura se promocionan a los espacios de sobrevivientes.
La cantidad de Survivor Spaces en Java Garbage Collector depende del recolector específico que se utilice. La cantidad de espacios de supervivencia depende del coleccionista específico que se utilice. En Parallel Collector y CMS Collector, hay varios espacios de supervivientes. El recopilador paralelo divide el espacio superviviente en varias regiones, mientras que el recopilador CMS utiliza varios espacios supervivientes. A continuación, analizaremos más de cerca los diferentes recolectores de basura de Java.
Vieja generación
Los objetos que sobreviven a un cierto número de recolecciones de basura se promocionan a la generación anterior. Los objetos de vieja generación tienen una vida útil más larga. No son elegibles para GC menores y solo se eliminan durante una recolección de basura importante.
La vieja generación también se llama generación estable.
Pasos involucrados en la recolección de basura.
La recolección de basura de Java funciona monitoreando continuamente la memoria del montón de la máquina virtual Java para identificar objetos que ya no están en uso.
La recolección de basura de Java se realiza en los siguientes pasos:
- Etiquetado: el GC primero identifica todos los objetos activos en la pila y los etiqueta.
- Escanear: una vez identificados y marcados todos los objetos activos, el GC escanea el montón y libera la memoria que ya no está en uso. La memoria estará entonces disponible para ser asignada a nuevos objetos.
- Compresión: en algunos algoritmos de recolección de basura de Java, después del escaneo, los objetos restantes se comprimen, lo que significa que se mueven a un extremo del montón, lo que facilita que la JVM asigne nuevos objetos.
Recolector de basura más pequeño
La recolección de basura menor es el proceso de identificar y recolectar basura de la generación más joven mientras se mantiene libre de basura y se reduce la frecuencia de los principales ciclos de recolección de basura de Java.
La recolección de basura secundaria de Java funciona en un tamaño de montón más pequeño y, por lo tanto, es considerablemente más rápida que la recolección de basura primaria.
Así es como funciona la recolección de basura menor:
Llenado del espacio Eden: a medida que se asignan nuevos objetos al espacio Eden, eventualmente se llena. Cuando el espacio de Eden se llena, el recolector de basura inicia un ciclo de GC más pequeño.
Marcado inicial: el recolector de basura comienza el ciclo de recolección de basura secundaria realizando una fase de marcado inicial. Durante esta fase, el recolector de basura identifica todos los objetos vivos en el espacio del Edén y en los espacios de supervivientes. El recolector de basura marca estos objetos vivos para indicar que no deben ser recogidos.
Recolección de copias: una vez completada la fase de marcado inicial, el recolector de basura realiza una fase de recolección de copias. Durante esta fase, el recolector de basura copia todos los objetos vivos del espacio del Edén y de uno de los espacios supervivientes al otro espacio superviviente.
Limpiar el espacio no utilizado: después de copiar los objetos activos al espacio superviviente, el recolector de basura limpia el espacio de Eden y el espacio superviviente que no se utilizó durante el ciclo de recolección de basura actual. Cualquier objeto que no se haya copiado en el espacio superviviente se considera basura y el recolector de basura lo reclama.
Promoción de objetos: los objetos que han sobrevivido a una cierta cantidad de ciclos de recolección de basura eventualmente serán promovidos a la generación anterior (también conocida como generación estable), donde serán administrados por un algoritmo de recolección de basura diferente optimizado para objetos vivos por más tiempo.
Ciclos múltiples: si el espacio superviviente que se utilizó durante el ciclo actual de recolección de basura de Java se llena, el recolector ejecuta ciclos secundarios adicionales de recolección de basura hasta que se hayan promovido suficientes objetos a la generación anterior o el espacio superviviente vuelva a quedar vacío.
Gran recolector de basura
Cuando el viejo espacio generacional se llena, llega el gran recolector de basura. Se llama "principal" porque funciona en todo el montón y se invoca con menos frecuencia que el recolector de basura secundario. Generalmente requiere más tiempo y recursos.
Los pasos involucrados en la recolección de basura central son muy similares a los descritos anteriormente.
Tipos de recolectores de basura en Java
Java ofrece algunos tipos diferentes de recolectores de basura. Aquí tienes una lista de todos los recolectores de basura, cómo funcionan y sus ventajas.
Recopilador en serie o detener y copiar
Serial Garbage Collector es un tipo de GCr en Java que utiliza un único subproceso para realizar el proceso de recolección de basura de Java. Se utiliza principalmente en aplicaciones básicas que tienen patrones de uso de memoria relativamente simples.
Como habrás adivinado, el recolector de basura en serie funciona de forma secuencial, lo que significa que detiene todos los subprocesos de la aplicación mientras se ejecuta el proceso de recolección de basura de Java. Esta pausa en la ejecución de la aplicación a veces se denomina evento "detener el mundo".
Para utilizar Serial Collector, pase -XX:UseSerialCollector como argumento.
Ejemplo, java -XX:UseSerialCollector SuPrograma
Ventajas del colector en serie:
- Simplicidad: este es el recolector de basura más simple y directo de Java. Ocupa poco espacio y requiere un ajuste mínimo.
- Previsibilidad: debido a que utiliza un solo hilo, su comportamiento es predecible y fácil de entender. Esto lo hace útil para aplicaciones que requieren un patrón de uso de memoria predecible.
- Gastos generales muy bajos: esto lo hace útil para aplicaciones pequeñas donde el rendimiento es crítico.
Desventajas del coleccionista en serie:
- No diseñado para escalar: No está diseñado para escalar con el tamaño del montón o la cantidad de procesadores disponibles en el sistema.
- Uso deficiente de la memoria: Proporciona una utilización deficiente de la memoria disponible.
- Tiempos de pausa largos: Debido a su diseño, se incorporan tiempos de pausa largos en el proceso.
Colector paralelo
El recolector de basura paralelo , que actúa como recolector de basura predeterminado en Java, es un método que utiliza múltiples subprocesos para aumentar el rendimiento de la recolección de basura. Es particularmente eficaz para aplicaciones más grandes con patrones de uso de memoria complejos.
Al subdividir el montón en segmentos más pequeños, Parallel Garbage Collector utiliza varios subprocesos para ejecutar el proceso de recolección de basura simultáneamente. Al igual que el recolector en serie, el recolector paralelo también provoca una pausa temporal en la ejecución de la aplicación durante la recolección de basura.
Para utilizar Parallel Collector, pase -XX:+UseParallelGC como argumento.
Ventajas del colector paralelo:
- Más rápido: proporciona un mejor rendimiento en comparación con los recolectores en serie debido al uso de múltiples subprocesos, lo que permite operaciones de recolección de basura más rápidas.
- Mejor escalabilidad: Diseñado para escalar de manera efectiva con el tamaño del montón, lo que lo hace adecuado para aplicaciones con mayores requisitos de memoria.
Desventajas del colector paralelo:
- Tiempos de pausa más largos: Parallel Garbage Collector detiene la aplicación durante el proceso de recolección de basura, lo que puede resultar en tiempos de pausa más largos en comparación con otros recolectores de basura.
- Mayor sobrecarga de CPU: Parallel Garbage Collector utiliza múltiples subprocesos, lo que puede resultar en una mayor sobrecarga y un mayor uso de memoria.
Recopilador de escaneo de marcas simultáneo
El recolector de basura CMS (etiquetado y escaneo simultáneos) es otro tipo de recolector de basura. Trabaja simultáneamente con la aplicación para realizar el proceso de recolección de basura o, dicho de otra manera, utiliza múltiples subprocesos recolectores de basura. Está diseñado para minimizar los tiempos de pausa de las aplicaciones y reducir el impacto en el rendimiento de la recolección de basura de Java.
Para usar CMSCollector, pase -XX:+UseConcMarkSweepGC como argumento.
Ventajas del CMS:
- Tiempos de pausa bajos: el CMS minimiza los tiempos de pausa durante la recolección de basura, brindando una experiencia más fluida para aplicaciones sensibles a la latencia.
- Predecible: CMS ofrece pausas de recolección de basura más predecibles, lo que puede ser crucial para sistemas o aplicaciones en tiempo real con requisitos de rendimiento estrictos.
- Adecuado para montones más grandes: CMS mantiene su rendimiento incluso cuando aumenta el tamaño del montón, lo que lo convierte en una opción viable para aplicaciones con demandas sustanciales de memoria.
Desventajas del CMS:
- Mayor sobrecarga de CPU: CMS consume más recursos de CPU debido a su naturaleza concurrente, lo que puede afectar el rendimiento general de la aplicación.
- Riesgo de fragmentación: CMS no es una opción ideal para aplicaciones de larga duración, ya que el montón puede fragmentarse con el tiempo. Esta fragmentación puede provocar un mayor uso de la memoria y una disminución del rendimiento.
Colector G1
Garbage First Collector (G1) es un algoritmo de recolección de basura introducido en Java 7, diseñado para abordar las limitaciones de los recolectores de basura tradicionales como Parallel Collector y CMS Collector. G1 está diseñado para ser un recopilador de baja pausa orientado al rendimiento que puede manejar montones muy grandes.
Para utilizar G1 Collector, pase el argumento:
-XX:+UsarG1GC
Ventajas del colector G1:
- Tiempos de pausa bajos: el G1 está diseñado para minimizar los tiempos de pausa, lo que puede hacerlo adecuado para aplicaciones en tiempo real.
- Escalabilidad: G1 es escalable, lo que lo hace adecuado para aplicaciones grandes con diferentes tamaños de montón.
Desventajas del coleccionista G1:
- Gastos generales: G1 consume más recursos de CPU en comparación con otros recolectores de basura, lo que genera una mayor sobrecarga de CPU.
- Tiempo de marcado inicial elevado: la fase de marcado inicial puede tardar más en G1, especialmente para tamaños de montón grandes, lo que puede afectar negativamente el rendimiento de la aplicación.
- No apto para montones pequeños : G1 Collector no es ideal para aplicaciones con montones pequeños porque sus beneficios se logran mejor en entornos de memoria más grandes.
ZGC
Z Garbage Collector (ZGC) es un recolector de basura de Java diseñado específicamente para administrar montones extremadamente grandes (hasta 16 TB) manteniendo tiempos de pausa mínimos. Su principal objetivo es minimizar la duración de los procesos de recolección de basura, maximizando así el rendimiento de la aplicación.
Ventajas de ZGC:
- Tiempos de pausa bajos: está diseñado para minimizar los tiempos de pausa, lo que lo hace adecuado para aplicaciones en tiempo real o sensibles a la latencia.
- Escalabilidad: Está diseñado para escalar según el tamaño del montón y la cantidad de procesadores disponibles.
- Alto rendimiento: está optimizado para un alto rendimiento, logrando un rendimiento sustancial y minimizando el impacto de la recolección de basura de Java en el rendimiento de la aplicación.
Desventajas de ZGC:
- Alta sobrecarga de memoria: ZGC requiere una cantidad significativa de memoria para funcionar de manera eficiente.
- Compatibilidad limitada: ZGC solo está disponible en determinadas plataformas, incluido Linux/x64, y requiere un mínimo de JDK 11.
- Mayor utilización de la CPU: debido a sus funciones avanzadas, ZGC puede consumir más recursos de la CPU en comparación con otros recolectores de basura, lo que podría afectar el rendimiento general de la aplicación.
Coleccionista de Shenandoah
Shenandoah es un recolector de basura de Java diseñado para proporcionar tiempos de pausa ultrabajos manteniendo un alto rendimiento. Como recolector de basura concurrente, opera en paralelo con la aplicación, lo que lo hace adecuado para aplicaciones sensibles a la latencia.
Para utilizar Shenandoah Collector, pase el argumento:
-XX:+DesbloquearOpcionesExperimentalVM -XX:+UsarShenandoahGC
Ventajas de Shenandoah:
- Tiempos de pausa ultrabajos: proporciona tiempos de pausa mínimos, lo que lo hace ideal para aplicaciones en tiempo real o sensibles a la latencia.
- Baja sobrecarga de memoria: administra la memoria de manera eficiente, reduciendo la sobrecarga de memoria.
- Alto rendimiento: a pesar de centrarse en tiempos de pausa bajos, Shenandoah aún mantiene un alto rendimiento, lo que garantiza un rendimiento óptimo de las aplicaciones.
Desventajas del Shenandoah:
- Compatibilidad limitada: Shenandoah solo está disponible en determinadas plataformas, incluido Linux/x64, y requiere un mínimo de JDK 8u40.
- Mayor utilización de la CPU: esto puede consumir más recursos de la CPU en comparación con otros recolectores de basura, lo que podría afectar el rendimiento general de la aplicación.
Sistema.gc
Runtime.getRuntime .gc o System.gc son métodos que solicitan a la JVM que realice una recolección de basura para liberar memoria (énfasis en la palabra "sugerir", ya que eso es exactamente lo que hace).
No se puede forzar la recolección de basura. Si obtiene " java.lang.OutOfMemoryError ", llamar a System,gc no resolverá el problema porque la JVM generalmente intenta ejecutar un recolector de basura antes de arrojar "java.lang.OutOfMemoryError". Las posibles soluciones podrían ser utilizar un GC diferente o aumentar el tamaño del montón.
En general, se recomienda que los desarrolladores eviten llamar a System.gc directamente y, en cambio, confíen en la recolección automática de basura proporcionada por la JVM.
Problemas comunes con la recolección de basura y cómo solucionarlos
Aquí hay algunos problemas que puede enfrentar:
Errores de falta de memoria:
Este error ocurre cuando la JVM se queda sin memoria. Para resolver este problema, los desarrolladores pueden aumentar el tamaño del montón u optimizar la aplicación para utilizar menos memoria.
- -Xmx: establece el tamaño máximo del montón para su aplicación.
- -Xms: establece el tamaño del montón inicial para su aplicación.
Por ejemplo, para establecer el tamaño máximo del montón en 2 gigabytes y el tamaño del montón inicial en 512 megabytes, puede utilizar el siguiente comando:
java -Xmx2g -Xms512m SuPrograma
Tiempos de pausa prolongados:
Los tiempos de pausa prolongados durante la recolección de basura pueden hacer que la aplicación deje de responder. Para resolver este problema, puede elegir un recolector de basura diseñado para tiempos de pausa bajos o ajustar los parámetros del recolector de basura.
Pérdida de memoria:
Las pérdidas de memoria ocurren cuando los objetos no se liberan adecuadamente de la memoria, lo que conduce a un mayor uso de la memoria con el tiempo. Para resolver este problema, los desarrolladores pueden utilizar herramientas como generadores de perfiles para identificar pérdidas de memoria y solucionarlas.
Mejores prácticas para gestionar la memoria en Java
Para evitar problemas comunes con la recolección de basura y administrar la memoria de manera eficiente, estas son algunas de las mejores prácticas:
- Establezca la referencia en nulo: establezca siempre la referencia en nulo cuando ya no necesite un objeto.
- Evite crear objetos innecesarios: la creación de objetos innecesarios puede aumentar el uso de memoria y provocar una recolección de basura más frecuente. Debe evitar crear objetos innecesarios y reutilizar los existentes cuando sea posible.
- Usar objeto anónimo: esto es cuando no almacena la referencia al objeto.
Ejemplo, crearUsuario(nuevo usuario).
- Libere recursos cuando ya no sean necesarios: los objetos que utilizan recursos externos, como identificadores de archivos o conexiones de bases de datos, deben liberarse cuando ya no sean necesarios para evitar pérdidas de memoria.
Conclusión
Comprender la mecánica de la recolección de basura de Java es esencial, ya sea que esté desarrollando internamente o decidiendo subcontratar el desarrollo de Java, especialmente si desea mejorar el rendimiento de su aplicación Java. Hemos analizado en profundidad esta parte importante de la programación Java, desde los conceptos básicos de cómo funciona la recolección de basura y los diferentes tipos de recolectores de basura hasta los puntos más finos de la administración de la memoria. Recuerde, incluso cuando subcontrate el desarrollo de Java, elegir el tipo correcto de recolección de basura de Java y administrar la memoria de manera eficiente marcará una gran diferencia en la velocidad de su aplicación. Siga explorando, siga codificando y recuerde que cada eficiencia puede contribuir a una aplicación más fluida y rápida. ¡Feliz codificación!
Si le gustó esto, asegúrese de consultar uno de nuestros otros artículos sobre Java:
- Ajuste del rendimiento de Java: 10 técnicas probadas para maximizar la velocidad de Java
- Las 7 mejores herramientas de creación de perfiles de Java para 2021
- Listado de las 9 mejores herramientas de análisis de código estático de Java
- Pruebas unitarias de Java con JUnit 5: mejores prácticas y técnicas explicadas
- Java x Kotlin: las principales diferencias explicadas
Preguntas frecuentes
¿Cómo pueden los desarrolladores identificar y diagnosticar pérdidas de memoria o ineficiencias en sus aplicaciones Java?
Los desarrolladores pueden identificar y diagnosticar pérdidas de memoria o ineficiencias en sus aplicaciones Java:
- Supervisar el uso de la memoria de la aplicación y los registros de GC mediante herramientas como JConsole, VisualVM o Java Flight Recorder.
- Analizar volcados de montón para identificar objetos que ocupan una cantidad excesiva de memoria.
- Usar herramientas de creación de perfiles como YourKit, JProfiler o Java Mission Control para identificar puntos de acceso y patrones de uso de memoria.
- Escriba y ejecute pruebas de rendimiento que simulen escenarios de uso del mundo real.
- Inspeccione el código y revise las mejores prácticas para la gestión de la memoria, como evitar la creación innecesaria de objetos, minimizar la retención de objetos y utilizar estructuras de datos adecuadas.
¿Cuáles son las compensaciones entre rendimiento, latencia y sobrecarga de memoria al seleccionar y configurar un recolector de basura para una aplicación Java?
Al elegir y configurar un recolector de basura que Java utiliza para una aplicación, los desarrolladores deben sopesar las ventajas y desventajas entre rendimiento, latencia y sobrecarga de memoria:
- Rendimiento: se refiere a la capacidad de la aplicación para procesar cargas de trabajo de manera eficiente. Los recolectores de basura que enfatizan el alto rendimiento pueden experimentar pausas más prolongadas durante la recolección de basura, lo que afecta la latencia de la aplicación.
- Latencia: este es el tiempo que tarda una aplicación en responder a una solicitud. Los recolectores de basura que se centran en una latencia baja pueden consumir más memoria para minimizar la frecuencia de las pausas en la recolección de basura, lo que genera una mayor sobrecarga de memoria.
- Sobrecarga de memoria: esto representa la cantidad de memoria que utiliza el GC para administrar la memoria de la aplicación. Es posible que los recolectores de basura que requieren menos memoria deban pausar la recolección de basura con más frecuencia, lo que puede afectar tanto el rendimiento como la latencia.
Los desarrolladores deben evaluar cuidadosamente estas compensaciones al seleccionar y configurar un recolector de basura para su aplicación Java, teniendo en cuenta los requisitos y limitaciones únicos de su caso de uso específico.
Fuente: BairesDev