🧪 Problem-Driven Systems Lab

☕ Caso 05 — Java 21#

⬅️ Caso 05 · ⚖️ Comparativa de los 7 stacks · ☕ Perfil de Java · 🧬 Todos los perfiles

Stack Java operativo del caso 05. Fuga real cross-request (ArrayList<byte[]> estatico) vs LRU acotada built-in (LinkedHashMap.removeEldestEntry).

Primitivas nativas#

PrimitivaRol
LinkedHashMap con removeEldestEntryLRU built-in del JDK — sin libreria, una linea para eviccion automatica.
Runtime.getRuntime().totalMemory()/freeMemory()/maxMemory()Medicion directa del heap del proceso. Sin agente, sin JFR.
System.gc() (hint, no garantia)Disponible en /reset-lab para forzar comparacion antes/despues.
Collections.synchronizedList/MapAcceso seguro desde el pool de handlers.

Contraste#

Legacy — leak real:

java
private static final List<byte[]> legacyAccumulator =
    Collections.synchronizedList(new ArrayList<>());

byte[] payload = new byte[sizeKb * 1024];
legacyAccumulator.add(payload);   // nunca se libera

Optimized — LRU acotada built-in:

java
private static final Map<Integer, byte[]> optimizedCache =
    Collections.synchronizedMap(new LinkedHashMap<>(OPTIMIZED_CAP, 0.75f, true) {
        @Override protected boolean removeEldestEntry(Map.Entry<Integer, byte[]> e) {
            return size() > OPTIMIZED_CAP;  // eviccion automatica
        }
    });

Rutas#

RutaQue muestra
/healthliveness
/batch-legacy?size_kb=64acumula en lista estatica — retained_count crece
/batch-optimized?size_kb=64LRU con cap=1000, evictions_total sube cuando rebasa
/statesnapshot del heap (heap_used_mb, heap_max_mb, retained counts)
/diagnostics/summarycontraste completo + runtime
/reset-lablimpia acumuladores + System.gc()

Hub#

docker compose -f compose.java.yml up -d --build
# generar presion legacy
for i in {1..50}; do curl -s "http://127.0.0.1:8400/05/batch-legacy?size_kb=128" > /dev/null; done
curl http://127.0.0.1:8400/05/state
# vs optimized — se mantiene estable
for i in {1..5000}; do curl -s "http://127.0.0.1:8400/05/batch-optimized?size_kb=64" > /dev/null; done
curl http://127.0.0.1:8400/05/state

Lo que el JVM mete en la ecuacion#

El heap del JVM lo maneja el GC. Runtime.totalMemory() retorna el heap actual asignado por la JVM (puede crecer hasta maxMemory()); freeMemory() es lo libre dentro de ese heap. Una "fuga" en Java no es que el sistema operativo pierda memoria — es que el GC no puede recolectar porque las referencias siguen alcanzables desde la raiz (static field). Eso es exactamente lo que demuestra legacyAccumulator.

Ver esta carpeta en GitHub ↗