Luchando contra el error java.lang.OutOfMemoryError: Java heap space
Hace poco tuve que lidiar con el temible error "java.lang.OutOfMemoryError: Java heap space" cuando tuve que implementar un pequeño programa en Java que consumía mensajes de unos topics de Apache Kafka, para después procesarlos y producirlos de nuevo en otro topic diferente. Como ya estaba en la fase de pruebas, tuve que buscar de una manera rápida una solución alternativa, y estos son los pasos que seguí.
Que es la Heap de Java:
Pero antes de nada, vamos a aclarar el concepto de Heap en Java.
La heap de Java es una parte de la memoria del sistema donde la Java Virtual Machine (JVM) almacena todos los objetos creados durante la ejecución de un programa. Es una zona de memoria dinámica: es decir, su contenido puede crecer o decrecer en tiempo de ejecución hasta el límite permitido.
En la heap se almacenan principalmente:
- Objetos instanciados con new
- Sus atributos (si también son objetos)
- Arrays (new int[10], new String[], etc...)
- Clases anónimas y lambdas, si contienen referencias
En cambio en el Heap no se guardan
- Variables locales primitivas (int, double, boolean, etc.) porque están en la pila, también llamada "stack".
- Las referencias a objetos pueden estar en el stack, pero el objeto apuntado está en la heap.
- Métodos y bytecode se cargan en otras áreas como el Metaspace.
Por lo tanto, como la Heap tiene un limite, si llegas a ocupar todo ese espacio de la memoria, tu programa va a dejar de funcionar.
Solucionando el problema de memoria
El programa en si era bastante sencillo, pero la complicación venía en que no había que consumir los mensajes nuevos que fuesen entrando al topic, sino que tenía que consumir siempre todos los mensajes que había en el topic desde el principio.
El topic en cuestion tenía una politica de retencion de 7 días, por lo que cada vez que se ejecutara el programa, tendría que consumir todos los mensajes de los últimos 7 días.
Mi primera solución para esto fue simplemente consumir todos los mensajes e ir metiendolos en una lista, para después recorrer esa lista, e ir modificando cada mensaje en base a unas especificaciones que debía cumplir. No podía procesarlos a la vez que los iba consumiendo, porque para procesarlos correctamente necesitaba saber la cantidad total de mensajes consumidos.
Una vez modificados los mensajes, simplemente había que producirlos en el topic final.
Cual fue mi sorpresa, cuando la primera vez que fui a probar el programa, me saltó el famoso error "java.lang.OutOfMemoryError: Java heap space".
El problema en este caso se pudo ver claramente cual era. En el topic del cual estaba consumiendo, había millones de mensajes, y obviamente al ir guardandolos en una lista que está en memoría, llegaba un punto en el que la aplicación llegaba a su límite.
Para tratar este problema, lo primero que me vino a la mente fue aumentar la RAM que usaba el programa usando los parámetros -Xmx y -Xms, pero eso no solucionaría mis problemas, ya que nadie me garantizaba la cantidad de mensajes que iba a tener ese topic, y siempre iba a poder ocurrir el mismo problema.
Así que lo que decidí finalmente fue utilizar un sistema de Cache externo, que no utilizase la memoria Heap. Para ello hice uso de "mapdb", que nos permite tener un Map que en lugar de almacenarse en memoria se almacena en un fichero físico.
Obviamente, si vas a manejar muchisimos datos, tendrás que asegurarte de que el servidor en el que estés trabajando tenga espacio suficiente para almacenarlos.
Para utilizar esta libreria, lo único que hay que hacer es importarla. Aquí te dejo el enlace del repositorio central de Maven: https://mvnrepository.com/artifact/org.mapdb/mapdb/3.0.10
Una vez importada, puedes inicializar la cache de una manera muy sencilla
DB db = DBMaker.fileDB("cache.db")
.fileMmapEnable()
.make();
ConcurrentMap map = db.hashMap("messagesConsumed")
.keySerializer(Serializer.STRING)
.valueSerializer(Serializer.STRING)
.createOrOpen();
Una vez inicializado el ConcurrentMap, podremos trabajar con el como si fuese un mapa normal y corriente, añadiendo elementos con un simple "put" y accediendo a ellos iterando sobre sus keys y values.
Por tanto, ahora en mi programa en lugar de guardar los mensajes consumidos en una lista, los guardé en el map y con esta simple solución, el programa pudo consumir millones de registros de Kafka sin incurrir en problemas de memoria.
Conclusión
Cuando veas que tienes problemas de memoria lo primero que tienes que hacer es localizar el foco del problema, y una vez localizado, buscar una alternativa en la cual uses menos memoria de la que estas utilizando.
En este caso yo tuve que utilizar una cache externa porque necesitaba guardar todos los mensajes para despues procesarlos, pero en otros casos el problema puede ser simplemente la creación de objetos innecesarios en memoria por un bucle mal creado y con una refactorizacion de tu código puedas solucionarlo.
Lo importante es no desesperarse e intentar no buscar como primera solución el aumento de la memoria del programa, ya que eso muchas veces enmascara el problema más que solucionarlo.
Un saludo!
2025-06-10