Memoria y recolector de basura en Kotlin: cómo no perder RAM ni rendimiento
\n\n¿Alguna vez te has preguntado a dónde va la memoria RAM de tu aplicación en Kotlin? El código parece estar escrito perfectamente, las corrutinas vuelan, pero después de una hora de funcionamiento, el programa comienza a "ir lento" o falla con OutOfMemoryError. ¿Un dolor familiar? Bienvenido al club de aquellos que quieren entender cómo Kotlin gestiona la memoria y por qué el recolector de basura no es el enemigo, sino tu mejor amigo (si sabes cómo llevarte bien con él).
En este artículo, nos sumergiremos en el tema de la gestión de memoria en Kotlin y analizaremos cómo funciona el recolector de basura en Kotlin en diferentes plataformas. Aprenderás qué son la pila y el montón, cómo evitar fugas y por qué WeakReference puede salvar tu proyecto. ¡Allá vamos!
¿Qué es la memoria en Kotlin? Pila, montón y sus amigos
\n\nKotlin es un lenguaje multiplataforma. Se compila a bytecode de la JVM, a código nativo (a través de Kotlin/Native) o a JavaScript (Kotlin/JS). Y en cada plataforma, la memoria está organizada de manera ligeramente diferente, pero la base es la misma en todas.
\n\nRecordemos los fundamentos. Cuando tu programa se inicia, el sistema operativo le asigna un área de memoria. Dentro de esta área hay dos "regiones" principales:
\n\n- \n
- Pila (Stack) — aquí se almacenan las variables locales, los primitivos y las referencias a objetos. Funciona como una pila de platos: rápido, predecible, pero con un tamaño limitado. Salirse de la pila provoca un
StackOverflowError. \n - Montón (Heap) — el vertedero para todos los objetos que creas mediante
newo un constructor (val list = mutableListOf()). El montón es grande, pero lento, y es aquí donde "vive" el recolector de basura de Kotlin. \n
En la JVM, la pila y el montón funcionan de manera clásica. En Kotlin/Native, la pila es la pila nativa del hilo, y el montón es gestionado por su propio recolector de basura. En Kotlin/JS, todo recae sobre el motor de JavaScript (V8, SpiderMonkey).
\n\nPunto clave: las referencias a objetos se almacenan en la pila, y los objetos mismos, en el montón. Mientras un objeto tenga al menos una referencia "viva" (desde la pila u otro objeto), el recolector de basura no lo tocará.
\n\nRecolector de basura en Kotlin: cómo funciona en la JVM
\n\nCuando escribes en Kotlin para la JVM (Android, servidor), obtienes el poder del Recolector de Basura de la Máquina Virtual Java (GC). No es algo específico de Kotlin, pero es necesario entender su mecánica.
\n\nLa JVM moderna utiliza G1GC (Garbage First) o ZGC (en versiones nuevas). Así funciona de manera simplificada:
\n\n- \n
- Mark (Marcado) — el GC "recorre" todos los objetos vivos, comenzando desde los puntos raíz (pila, campos estáticos). Todo lo que alcanza está vivo. \n
- Sweep (Barrido) — todo lo que no está marcado se considera basura y se elimina. \n
- Compact (Compactación) — los objetos vivos se desplazan para evitar la fragmentación. \n
En G1GC, el montón está dividido en regiones. El GC recolecta aquellas regiones donde hay más basura (de ahí el nombre). Esto reduce las pausas, pero no las elimina por completo.
\n\nEn Kotlin, puedes influir en el recolector de basura a través de las banderas de la JVM (para servidores) o mediante GarbageCollectorMXBean (para monitoreo). Pero la mayoría de las veces, solo necesitas escribir código que no interfiera con el trabajo del GC.
Ejemplo: cómo ve el GC tu código
\n\nfun processData() {\n val list = mutableListOf<String>()\n for (i in 1..100_000) {\n list.add("Element $i")\n }\n // Aquí list está vivo, hasta que el método termine\n println(list.size)\n // Después de salir del método, list se convierte en basura\n}\n\n\nDespués de que processData() finaliza, la referencia a list desaparece de la pila, y el objeto se convierte en candidato para la recolección. Sencillo, ¿verdad?
Particularidades de Kotlin/Native: recolector de basura sin JVM
\n\nKo