Skip to content

Instantly share code, notes, and snippets.

@alonsoir
Last active September 4, 2026 06:43
Show Gist options
  • Select an option

  • Save alonsoir/6f9c970ee3175db434c6f007582a3336 to your computer and use it in GitHub Desktop.

Select an option

Save alonsoir/6f9c970ee3175db434c6f007582a3336 to your computer and use it in GitHub Desktop.
Qué hacer si la app spring-boot se come la RAM?
Atencion, pregunta:
Cuando ves que un servicio en Spring Boot se está comiendo la RAM,
el 90% de las veces la culpa no es del framework, sino de cómo le metimos mano.
Antes de saltar a pagar un servidor más grande para solucionar el problema a billetazos,
suelo revisar 4 cosas clave: -El Garbage Collector:
Si manejas heaps grandes y necesitas que la app no se "congele",
dale una oportunidad a ZGC o Shenandoah en lugar del que viene por defecto.
-Proyecciones en JPA: Si solo necesitas tres datos en pantalla, no te traigas la entidad completa de la base de datos
con todas sus relaciones. Usa DTOs.
-Procesar en batches: Cargar listas gigantescas de golpe a memoria es buscarse un OutOfMemoryError.
Procesa todo paginado o por lotes.
-El pool de HikariCP: No abras hilos a lo loco. Dimensiona bien las conexiones para no dejar memoria tirada en
hilos que ni se están usando.
-Entender qué está haciendo la JVM por detrás es la diferencia entre arreglar un bug o solo taparlo
pagando más nube. Si ves un pico raro de RAM en producción, ¿qué es lo primerito que entras a revisar?
Respuesta:
Lo primerito: **mirar la forma del consumo de memoria y confirmar si es realmente el heap de Java**.
Antes de tocar GC, JPA o Hikari, revisaría:
1. **Heap usado vs. heap máximo**
* ¿La memoria sube y baja normalmente tras los GC?
* ¿O sube continuamente y nunca vuelve a bajar? Eso huele a fuga o a retención de objetos.
2. **Memoria fuera del heap**
* Metaspace.
* Direct buffers / NIO.
* Stacks de threads.
* Memoria nativa de librerías.
* Cachés.
Muchas veces `docker stats` dice que el proceso consume 8 GB, mientras el heap de Java solo ocupa 3 GB.
3. **Número de threads**
Un crecimiento inesperado de threads puede consumir bastante memoria por sus stacks y, además, ser síntoma de pools mal configurados.
4. **Heap dump, si el problema persiste**
Ahí dejaría de especular. Ver qué clases dominan la memoria y, sobre todo, **por qué siguen referenciadas** suele señalar directamente al culpable.
Mi orden mental sería:
> **¿Es heap o memoria nativa? → ¿Es un pico temporal o crecimiento sostenido? → ¿Qué objetos o threads están reteniendo memoria? → Después optimizo GC, JPA, batches o pools.**
Porque cambiar G1 por ZGC, por ejemplo, puede mejorar las pausas, pero **no arregla una aplicación que está acumulando objetos que nunca libera**.
Y una pequeña puntualización a tu texto: mencionas “4 cosas clave”, pero en realidad enumeras **5**. 😄
El script debería detectar automáticamente:
PID de la JVM.
RSS real del proceso.
Heap: used, committed, max.
GC y sus contadores.
Metaspace y Code Cache.
Número de threads y consumo potencial de stacks.
Direct buffers.
Memoria nativa mediante NMT, si está habilitado.
Límites y uso de memoria del cgroup v2.
Si el contenedor está acercándose al límite de Kubernetes.
Histogramas de objetos.
Evolución temporal para distinguir un pico de una fuga.
Ejecución en Kubernetes
Si el contenedor ya incluye las herramientas del JDK:
kubectl exec -it <pod> -c <container> -- bash
Y dentro:
chmod +x jvm-doctor.sh
./jvm-doctor.sh 1 10 60
Esto tomaría 60 muestras cada 10 segundos, durante diez minutos.
El reporte generado en bruto, sería algo así:
=== JVM MEMORY REPORT ===
Process:
PID: 12345
Command: java -Xmx4g ...
Operating system memory:
RSS: 5.8 GB
VSZ: 12.4 GB
Java Heap:
Used: 3.2 GB
Committed: 3.8 GB
Max: 4.0 GB
GC:
Collector: G1
Young GC: 124
Full GC: 2
Threads:
Total: 438
Top heap consumers:
byte[] 1.2 GB
java.lang.String 620 MB
com.myapp.UserEntity 480 MB
...
Assessment:
WARNING: Heap utilisation is growing and does not return
to its baseline after garbage collection.
Possible object retention or memory leak.
#!/usr/bin/env bash
#
# jvm-doctor.sh
# Diagnóstico básico de memoria JVM en Linux/Kubernetes
#
# Uso:
# ./jvm-doctor.sh [PID] [INTERVALO_SEGUNDOS] [MUESTRAS]
#
# Ejemplo:
# ./jvm-doctor.sh 1 10 30
#
set -u
PID="${1:-1}"
INTERVAL="${2:-10}"
SAMPLES="${3:-30}"
JCMD="$(command -v jcmd || true)"
JSTAT="$(command -v jstat || true)"
PS="$(command -v ps || true)"
if ! kill -0 "$PID" 2>/dev/null; then
echo "ERROR: El PID $PID no existe o no es accesible."
exit 1
fi
if [[ -z "$JCMD" ]]; then
echo "ERROR: jcmd no está disponible."
echo "Este diagnóstico necesita una imagen con JDK, no solo JRE."
exit 1
fi
bytes_to_mb() {
awk -v b="$1" 'BEGIN { printf "%.2f", b / 1024 / 1024 }'
}
bytes_to_gb() {
awk -v b="$1" 'BEGIN { printf "%.2f", b / 1024 / 1024 / 1024 }'
}
echo "======================================================"
echo " JVM DOCTOR - MEMORY DIAGNOSTIC"
echo "======================================================"
echo
echo "Timestamp: $(date -Is)"
echo "PID: $PID"
echo "Kernel: $(uname -r)"
echo
echo "---------------- PROCESS ----------------"
if [[ -n "$PS" ]]; then
$PS -p "$PID" -o pid,ppid,%cpu,%mem,rss,vsz,etime,args
fi
RSS_KB="$(awk '/VmRSS:/ {print $2}' /proc/$PID/status 2>/dev/null || true)"
THREADS="$(awk '/Threads:/ {print $2}' /proc/$PID/status 2>/dev/null || true)"
echo
echo "RSS:"
[[ -n "$RSS_KB" ]] && echo " $(awk -v k="$RSS_KB" 'BEGIN {printf "%.2f MB\n", k/1024}')"
echo "Threads:"
echo " ${THREADS:-unknown}"
echo
echo "---------------- CGROUP MEMORY ----------------"
# Kubernetes moderno normalmente utiliza cgroup v2.
if [[ -f /sys/fs/cgroup/memory.current ]]; then
CURRENT="$(cat /sys/fs/cgroup/memory.current)"
MAX="$(cat /sys/fs/cgroup/memory.max)"
echo "cgroup version: v2"
echo "Current: $(bytes_to_mb "$CURRENT") MB"
if [[ "$MAX" == "max" ]]; then
echo "Limit: unlimited"
else
echo "Limit: $(bytes_to_mb "$MAX") MB"
PERCENT="$(awk -v c="$CURRENT" -v m="$MAX" \
'BEGIN { printf "%.2f", (c/m)*100 }')"
echo "Usage: ${PERCENT}%"
if awk -v p="$PERCENT" 'BEGIN { exit !(p >= 90) }'; then
echo "WARNING: Container memory is above 90%"
elif awk -v p="$PERCENT" 'BEGIN { exit !(p >= 75) }'; then
echo "WARNING: Container memory is above 75%"
fi
fi
else
echo "cgroup v1 or unsupported layout"
fi
echo
echo "---------------- JVM HEAP ----------------"
"$JCMD" "$PID" GC.heap_info 2>/dev/null || \
echo "Unable to obtain GC.heap_info"
echo
echo "---------------- JVM NATIVE MEMORY ----------------"
"$JCMD" "$PID" VM.native_memory summary 2>/dev/null || \
echo "Native Memory Tracking is not enabled."
echo
echo "---------------- JVM FLAGS ----------------"
"$JCMD" "$PID" VM.flags 2>/dev/null || true
echo
echo "---------------- GC STATISTICS ----------------"
if [[ -n "$JSTAT" ]]; then
"$JSTAT" -gcutil "$PID" 2>/dev/null || \
echo "Unable to obtain jstat GC statistics."
else
echo "jstat unavailable."
fi
echo
echo "---------------- BUFFER POOLS ----------------"
"$JCMD" "$PID" PerfCounter.print 2>/dev/null | \
grep -Ei 'buffer|direct|mapped' || \
echo "No buffer pool counters found."
echo
echo "---------------- THREAD SUMMARY ----------------"
"$JCMD" "$PID" Thread.print 2>/dev/null | \
grep '^"' | \
sed 's/^"//' | \
cut -d'"' -f1 | \
sort | \
uniq -c | \
sort -nr | \
head -30 || true
echo
echo "---------------- TOP HEAP OBJECTS ----------------"
"$JCMD" "$PID" GC.class_histogram 2>/dev/null | \
head -40 || \
echo "Unable to obtain class histogram."
echo
echo "======================================================"
echo " TEMPORAL MEMORY SAMPLING"
echo " Samples: $SAMPLES"
echo " Interval: ${INTERVAL}s"
echo "======================================================"
printf "%-25s %-12s %-12s %-12s\n" \
"TIMESTAMP" "RSS_MB" "CGROUP_MB" "THREADS"
declare -a RSS_HISTORY
declare -a CGROUP_HISTORY
for ((i=1; i<=SAMPLES; i++)); do
NOW="$(date '+%Y-%m-%d %H:%M:%S')"
RSS_KB="$(awk '/VmRSS:/ {print $2}' \
/proc/$PID/status 2>/dev/null || echo 0)"
RSS_MB="$(awk -v k="$RSS_KB" \
'BEGIN {printf "%.2f", k/1024}')"
THREADS="$(awk '/Threads:/ {print $2}' \
/proc/$PID/status 2>/dev/null || echo 0)"
if [[ -f /sys/fs/cgroup/memory.current ]]; then
CGROUP_BYTES="$(cat /sys/fs/cgroup/memory.current)"
CGROUP_MB="$(bytes_to_mb "$CGROUP_BYTES")"
else
CGROUP_MB="N/A"
fi
RSS_HISTORY+=("$RSS_MB")
CGROUP_HISTORY+=("$CGROUP_MB")
printf "%-25s %-12s %-12s %-12s\n" \
"$NOW" \
"$RSS_MB" \
"$CGROUP_MB" \
"$THREADS"
if (( i < SAMPLES )); then
sleep "$INTERVAL"
fi
done
echo
echo "======================================================"
echo " BASIC ASSESSMENT"
echo "======================================================"
if (( ${#RSS_HISTORY[@]} >= 2 )); then
FIRST="${RSS_HISTORY[0]}"
LAST="${RSS_HISTORY[${#RSS_HISTORY[@]}-1]}"
DELTA="$(awk -v a="$FIRST" -v b="$LAST" \
'BEGIN {printf "%.2f", b-a}')"
PERCENT="$(awk -v a="$FIRST" -v b="$LAST" \
'BEGIN {
if (a > 0)
printf "%.2f", ((b-a)/a)*100
else
print "0"
}')"
echo "Initial RSS: ${FIRST} MB"
echo "Final RSS: ${LAST} MB"
echo "Delta: ${DELTA} MB (${PERCENT}%)"
if awk -v p="$PERCENT" 'BEGIN { exit !(p > 20) }'; then
echo
echo "WARNING:"
echo "RSS grew significantly during the observation period."
echo "Possible causes:"
echo " - Object retention / memory leak"
echo " - Growing caches"
echo " - Direct/native memory"
echo " - Thread growth"
echo " - Workload increase"
echo
echo "Compare this with heap_info and native_memory."
else
echo
echo "RSS remained relatively stable."
fi
fi
echo
echo "======================================================"
echo " END OF REPORT"
echo "======================================================"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment