Rendimiento en Java: diagnosticar antes de optimizar
La mayoría de los problemas de rendimiento en aplicaciones Java empresariales no están en el código Java, sino en la base de datos, en los pools de conexiones y en llamadas remotas sin timeout.
Medir primero, optimizar después
Optimizar por intuición produce cambios que no mejoran nada y que además añaden complejidad permanente.
En aplicaciones Java empresariales el reparto de causas es bastante estable. Por orden de frecuencia: consultas a base de datos —especialmente el problema N+1 y la ausencia de índices—, llamadas remotas sin timeout ni paralelismo, contención en pools de conexiones, serialización innecesaria y, bastante por detrás, el propio código Java.
La secuencia que funciona es siempre la misma: reproducir con datos reales, medir con trazas distribuidas para localizar la fase lenta, perfilar esa fase concreta, cambiar una sola cosa y volver a medir. Cambiar varias a la vez impide saber cuál ha tenido efecto.
Latencia y rendimiento agregado no son lo mismo
La latencia es lo que tarda una operación individual; el rendimiento agregado es cuántas operaciones se completan por unidad de tiempo. Optimizar uno puede empeorar el otro, así que el objetivo debe elegirse de forma explícita.
El error de medición más común es fijarse en la media. Una latencia media de 200 ms puede ocultar que el percentil 99 está en 8 segundos, y ese percentil es el que sufren precisamente los clientes con más datos, que suelen ser los más importantes.
En sistemas de cara al usuario conviene definir el objetivo sobre percentiles altos (p95 o p99) y sobre la operación completa de extremo a extremo, no sobre componentes aislados. Un servicio con p99 de 50 ms no significa nada si la petición atraviesa seis servicios.
Ajustar el recolector de basura, cuando de verdad es el problema
Se ajusta eligiendo el recolector según el objetivo —G1 como opción general, ZGC cuando las pausas máximas importan más que el rendimiento agregado— y dimensionando el heap a partir de medición real, no por estimación.
Antes de tocar parámetros conviene comprobar si el recolector es realmente el problema. Con los logs de GC activados se ve el porcentaje de tiempo en pausa; si está por debajo del uno por ciento, el cuello de botella está en otro sitio y ajustar el recolector no cambiará nada.
Cuando el problema sí es el recolector, la causa suele ser la tasa de asignación de objetos, no la configuración: código que crea objetos innecesarios en bucles calientes, colecciones intermedias en cadenas de streams o mapeos repetidos entre DTO. Reducir la asignación tiene más efecto que cualquier parámetro.
Pools y timeouts: la causa más frecuente de caídas en cascada
Un pool grande no aumenta el rendimiento si la base de datos no puede atender esa concurrencia: solo traslada la cola.
Tamaño del pool
Acorde a la concurrencia que la base de datos soporta, no al número de hilos de la aplicación.
Timeout de obtención
Para fallar rápido en lugar de acumular peticiones esperando una conexión que no llega.
Timeouts de red
De conexión y de lectura en todos los clientes HTTP: sin ellos, el valor por defecto es esperar indefinidamente.
Circuit breakers
Hacia las dependencias externas, para que una lentitud ajena no consuma todos los recursos propios.
La ausencia de timeouts es, con diferencia, la causa más frecuente de caídas totales: un sistema externo se ralentiza, las peticiones se acumulan, el pool se agota y el servicio deja de responder por completo.
Virtual threads: qué mejoran y qué no
Mejoran la escalabilidad de servicios dominados por espera de entrada/salida, permitiendo atender muchas más peticiones concurrentes con menos memoria. No aceleran el cálculo: una tarea intensiva en CPU tarda exactamente lo mismo.
El beneficio aparece cuando los hilos pasan la mayor parte del tiempo bloqueados esperando a la base de datos o a un servicio remoto. Con hilos de plataforma, cada petición retiene un hilo del sistema operativo; con virtual threads, ese coste desaparece y la concurrencia deja de estar limitada por el tamaño del pool.
Hay dos condiciones que conviene verificar antes de adoptarlos: que el código no use bloques synchronized en las rutas que bloquean —eso ancla el hilo virtual a su portador y anula la ventaja— y que los pools de recursos aguas abajo estén dimensionados, porque atender más peticiones concurrentes solo traslada la presión a la base de datos si no se ha ampliado su capacidad.
Preguntas frecuentes
Dudas habituales sobre java performance en proyectos empresariales.
¿Cómo se diagnostica un problema de rendimiento en Java?
Midiendo antes de cambiar nada: identificar dónde se va el tiempo con trazas distribuidas y perfilado, y solo después decidir qué optimizar. En aplicaciones empresariales las causas más frecuentes son, por este orden, las consultas a base de datos con problemas N+1 o sin índices, las llamadas remotas sin timeout, la contención en pools de conexiones, la serialización innecesaria y, bastante después, el propio código Java.
¿Qué diferencia hay entre latencia y rendimiento agregado?
La latencia es lo que tarda una operación individual y el rendimiento agregado es cuántas operaciones se completan por unidad de tiempo; optimizar uno puede empeorar el otro. El error de medición más común es fijarse en la media: una latencia media de 200 ms puede ocultar un percentil 99 de 8 segundos, que suelen sufrir precisamente los clientes con más datos.
¿Cómo se ajusta el recolector de basura de la JVM?
Eligiendo el recolector según el objetivo —G1 como opción general y ZGC cuando las pausas máximas importan más que el rendimiento agregado— y dimensionando el heap a partir de medición real. Antes de tocar parámetros conviene comprobar con los logs de GC si el recolector es realmente el problema: si el tiempo en pausa está por debajo del uno por ciento, el cuello de botella está en otro sitio.
¿Cómo se configuran los pools de conexiones en Java?
Dimensionando el pool según la capacidad real de la base de datos, no según el número de hilos de la aplicación, y estableciendo timeouts en todos los niveles: obtención de conexión, conexión y lectura en los clientes HTTP, más circuit breakers hacia dependencias externas. La ausencia de timeouts es la causa más frecuente de caídas en cascada.
¿Los virtual threads de Java 21 mejoran el rendimiento?
Mejoran la escalabilidad de servicios dominados por espera de entrada/salida, permitiendo atender muchas más peticiones concurrentes con menos memoria, pero no aceleran el cálculo. Antes de adoptarlos conviene verificar que el código no usa bloques synchronized en las rutas que bloquean, porque eso ancla el hilo virtual a su portador, y que los recursos aguas abajo estén dimensionados.
Recursos relacionados
Páginas pilar, guías y casos reales que amplían lo tratado aquí.
- Java Cloud Configuración de la JVM en contenedores.
- Guía de observabilidad Cómo medir para poder diagnosticar.
- Java Microservicios Latencia y fallos en sistemas distribuidos.
- Auditoría técnica Evaluación de rendimiento y escalabilidad.
- Spring Boot Transacciones, pools y errores frecuentes.
- Java Enterprise Requisitos de alta carga en plataformas críticas.
¿Tienes un problema de rendimiento que no logras localizar?
Instrumento la plataforma, localizo el cuello de botella real con datos y propongo las correcciones ordenadas por impacto.