La gestión de memoria de Java, con su recolección de basura incorporada, es uno de los mejores logros del lenguaje. Permite a los desarrolladores crear nuevos objetos sin preocuparse explícitamente de la asignación y desasignación de memoria, ya que el recolector de basura recupera automáticamente la memoria para su reutilización. Esto permite un desarrollo más rápido con menos código repetitivo, a la vez que elimina las fugas de memoria y otros problemas relacionados con la memoria. Al menos en teoría.
Irónicamente, la recolección de basura de Java parece funcionar demasiado bien, creando y eliminando demasiados objetos. La mayoría de los problemas de gestión de la memoria se resuelven, pero a menudo a costa de crear graves problemas de rendimiento. Hacer que la recolección de basura se adapte a todo tipo de situaciones ha llevado a un sistema complejo y difícil de optimizar. Para entender la recolección de basura, primero hay que comprender cómo funciona la gestión de la memoria en una máquina virtual Java (JVM).
Cómo funciona realmente la recolección de basura de Java
Mucha gente piensa que la recolección de basura recoge y descarta los objetos muertos. En realidad, ¡la recolección de basura de Java hace lo contrario! Se rastrean los objetos vivos y todo lo demás se designa como basura. Como verás, este malentendido fundamental puede llevar a muchos problemas de rendimiento.
Comencemos con el heap, que es el área de memoria utilizada para la asignación dinámica. En la mayoría de las configuraciones, el sistema operativo asigna el heap por adelantado para que sea gestionado por la JVM mientras se ejecuta el programa. Esto tiene un par de ramificaciones importantes:
- La creación de objetos es más rápida porque la sincronización global con el sistema operativo no es necesaria para cada objeto. Una asignación simplemente reclama alguna porción de una matriz de memoria y mueve el puntero de desplazamiento hacia adelante (ver Figura 2.1). La siguiente asignación comienza en este desplazamiento y reclama la siguiente porción de la matriz.
- Cuando un objeto ya no se utiliza, el recolector de basura reclama la memoria subyacente y la reutiliza para futuras asignaciones de objetos. Esto significa que no hay un borrado explícito y que no se devuelve memoria al sistema operativo.
- Las variables locales se mantienen vivas por la pila de un hilo. Esto no es una referencia virtual de objeto real y por lo tanto no es visible. A todos los efectos, las variables locales son raíces GC.
- Los hilos activos de Java siempre se consideran objetos vivos y, por tanto, son raíces GC. Esto es especialmente importante para las variables locales de los hilos.
- Las variables estáticas son referenciadas por sus clases. Este hecho las convierte en raíces GC de facto. Las propias clases pueden ser recolectadas como basura, lo que eliminaría todas las variables estáticas referenciadas. Esto es de especial importancia cuando utilizamos servidores de aplicaciones, contenedores OSGi o cargadores de clases en general. Discutiremos los problemas relacionados en la sección de Patrones de Problemas.
- Las Referencias JNI son objetos Java que el código nativo ha creado como parte de una llamada JNI. Los objetos así creados son tratados de forma especial porque la JVM no sabe si está siendo referenciada por el código nativo o no. Tales objetos representan una forma muy especial de raíz GC, que examinaremos con más detalle en la sección de Patrones de Problemas más adelante.
- Variables locales en el método principal
- El hilo principal
- Variables estáticas de la clase principal
Figura 2.1: Los nuevos objetos se asignan simplemente al final del heap utilizado.
Todos los objetos se asignan en el área del heap gestionada por la JVM. Cada elemento que el desarrollador utiliza es tratado de esta manera, incluyendo los objetos de clase, las variables estáticas, e incluso el propio código. Mientras un objeto sea referenciado, la JVM lo considera vivo. Una vez que un objeto deja de ser referenciado y por lo tanto no es alcanzable por el código de la aplicación, el recolector de basura lo elimina y reclama la memoria no utilizada. Aunque esto suene simple, plantea una pregunta: ¿cuál es la primera referencia en el árbol?
Raíces de la colección de basura: el origen de todos los árboles de objetos
Todo árbol de objetos debe tener uno o más objetos raíz. Mientras la aplicación pueda alcanzar esas raíces, todo el árbol es alcanzable. Pero, ¿cuándo se consideran alcanzables esos objetos raíz? Los objetos especiales llamados raíces de recolección de basura (raíces GC; véase la Figura 2.2) son siempre alcanzables y también lo es cualquier objeto que tenga una raíz de recolección de basura en su propia raíz.
Hay cuatro tipos de raíces GC en Java:
Figura 2.2: Las raíces GC son objetos que son a su vez referenciados por la JVM y, por lo tanto, evitan que todos los demás objetos sean recolectados por la basura.
Por lo tanto, una aplicación Java simple tiene las siguientes raíces GC:
Marcando y barriendo la basura
Para determinar qué objetos ya no están en uso, la JVM ejecuta intermitentemente lo que se llama muy acertadamente un algoritmo de marcar y barrer. Como se puede intuir, es un proceso sencillo de dos pasos:
- El algoritmo recorre todas las referencias a objetos, empezando por las raíces de la GC, y marca cada objeto encontrado como vivo.
- Toda la memoria de la pila que no está ocupada por los objetos marcados se recupera. Simplemente se marca como libre, esencialmente se barre para liberar los objetos no utilizados.
La recolección de basura pretende eliminar la causa de las clásicas fugas de memoria: objetos no alcanzables pero no eliminados en la memoria. Sin embargo, esto funciona sólo para las fugas de memoria en el sentido original. Es posible tener objetos no utilizados que todavía son alcanzables por una aplicación porque el desarrollador simplemente se olvidó de desreferenciarlos. Estos objetos no pueden ser recolectados por la basura. Y lo que es peor, una fuga de memoria lógica de este tipo no puede ser detectada por ningún software (véase la figura 2.3). Incluso el mejor software de análisis sólo puede destacar los objetos sospechosos. Examinaremos el análisis de las fugas de memoria en la sección Análisis del impacto en el rendimiento de la utilización de la memoria y la recolección de basura, más adelante.