Memoria JVM en Tomcat, Solr y Java
Cómo ajustar el heap, comprobar la configuración y reproducir una fuga de memoria con un ejercicio local.
Cuánta memoria necesita la JVM
-Xms fija el tamaño inicial del heap y -Xmx su máximo. Los objetos viven en el heap. Metaspace, pilas de hilos, buffers directos y otras estructuras también consumen memoria. Deja margen respecto a la RAM del servidor o al límite del contenedor. En Solr, reserva además espacio para la caché de archivos de Lucene.
Aumentar el heap de Tomcat
Si arrancas con catalina.sh, edita $CATALINA_BASE/bin/setenv.sh. Conserva las opciones existentes y sustituye cualquier -Xms o -Xmx anterior. CATALINA_OPTS aplica al proceso servidor. Reinicia con el mecanismo habitual y verifica el proceso nuevo. Si el servicio invoca Java directamente, ajusta las opciones en su configuración de arranque.
CATALINA_OPTS="${CATALINA_OPTS:-} -Xms512m -Xmx2g"
Aumentar el heap de Solr
En Linux, edita el solr.in.sh que lee tu servicio. En instalaciones estándar como servicio suele ser /etc/default/solr.in.sh. SOLR_HEAP fija el tamaño inicial y máximo. Si ya usas SOLR_JAVA_MEM, actualiza esa variable para evitar configuraciones que se solapen. Reinicia y comprueba los valores efectivos.
SOLR_HEAP="2g"
Ajustar una aplicación Java
Coloca las opciones de la JVM antes de -jar. Los tamaños son ejemplos. Con HotSpot moderno en contenedores, un -Xmx explícito fija el máximo. Si prefieres dimensionarlo a partir del límite que detecta la JVM, puedes usar -XX:MaxRAMPercentage=60.0 sin fijar -Xmx.
java -Xms256m -Xmx1g -jar app.jar
Comprobar el heap y observar el GC
jcmd -l lista procesos Java visibles. Sustituye 1234 por el PID y ejecuta jcmd con el mismo usuario del servicio en su entorno. VM.flags muestra los valores efectivos, como MaxHeapSize en bytes. GC.heap_info da una instantánea del heap. En Linux, ps muestra RSS en KiB para el proceso completo.
jcmd -l
jcmd 1234 VM.flags
jcmd 1234 GC.heap_info
ps -p 1234 -o pid,rss,comm
Con JDK 11 o posterior, añade esta opción al arranque para guardar registros de GC con rotación. Compara la ocupación tras las recolecciones, las pausas y la latencia bajo la misma carga antes y después del cambio.
-Xlog:gc*:file=gc.log:time,uptime,level,tags:filecount=5,filesize=10M
Cuando la memoria sigue creciendo
Si el heap crece después de las recolecciones, investiga referencias retenidas. Si el heap se estabiliza pero aumenta el RSS, revisa hilos, buffers y memoria nativa. Activa Native Memory Tracking al arrancar con -XX:NativeMemoryTracking=summary. Toma una línea base y compara después de aplicar carga. NMT cubre asignaciones internas de HotSpot y no explica todo el RSS.
jcmd 1234 VM.native_memory baseline
jcmd 1234 VM.native_memory summary.diff scale=MB
Una grabación JFR breve ayuda a localizar dónde se asignan objetos. Si necesitas investigar qué los mantiene vivos, analiza un volcado del heap. Puedes preparar -XX:+HeapDumpOnOutOfMemoryError y -XX:HeapDumpPath=/ruta/privada con espacio suficiente. Un volcado puede contener datos de la aplicación y generarlo en caliente puede causar una pausa.
jcmd 1234 JFR.start name=memory settings=profile duration=60s filename=memory.jfr
Prueba local de retención de objetos
En tus apuntes, CustomerManager devuelve el primer cliente con get(0), así que sigue en la lista. Esta versión reproduce la diferencia con bloques de 1 MiB. Guarda el código como HeapRetention.java. En modo retain, el programa conserva las referencias y acaba agotando un heap de 32 MiB. En modo drain, extrae cada bloque de la cola para que pueda ser recolectado.
import java.util.ArrayDeque;
import java.util.Queue;
class HeapRetention {
public static void main(String[] args) {
boolean retain = args.length > 0 && args[0].equals("retain");
Queue<byte[]> pending = new ArrayDeque<>();
for (int i = 0; i < 96; i++) {
pending.add(new byte[1024 * 1024]);
if (retain) {
pending.element();
} else {
pending.remove();
}
}
System.out.println("Pending blocks: " + pending.size());
}
}
Ejecuta los modos por separado. El primero imprime cero bloques pendientes. El segundo provoca un OutOfMemoryError de forma controlada. Abrir más heap retrasaría ese error mientras la cola siga creciendo.
java -Xms32m -Xmx32m -Xlog:gc HeapRetention.java drain
java -Xms32m -Xmx32m -Xlog:gc HeapRetention.java retain