1. Introducción al perfilado
El perfilado es como un chequeo médico para tu programa: no solo miramos la «temperatura» (monitoreo), sino que buscamos dónde «duele» la aplicación, qué funciona lento, dónde se consume demasiada memoria o recursos.
El perfilado es el proceso de recopilar y analizar información sobre el funcionamiento de un programa con el fin de detectar cuellos de botella (bottlenecks) y partes ineficientes del código. A diferencia del monitoreo, que suele seguir indicadores generales (carga de CPU, memoria, número de hilos), el perfilado permite mirar por dentro: saber qué métodos se llaman con más frecuencia, cuánto tiempo consumen, cuántos objetos se crean y dónde exactamente se produce una fuga de memoria.
¿Cuándo hace falta realmente el perfilado?
- La aplicación «se arrastra», pero no se entiende por qué.
- El consumo de memoria creció de repente.
- Después de actualizar el código, algo tarda más en ejecutarse.
- Hace falta entender por qué el servidor no tiene suficientes recursos.
Por cierto, casi todos los desarrolladores han optimizado al menos una vez la parte equivocada del código. ¿Por qué? Porque «a ojo» es casi imposible determinar el cuello de botella: para eso está el perfilador.
Métricas principales del perfilado
- Tiempo de ejecución de métodos (perfilado de CPU): ¿Qué métodos consumen más tiempo? ¿Dónde «gasta» CPU el programa?
- Uso de memoria (perfilado de memoria): ¿Qué objetos se crean con más frecuencia? ¿Dónde permanecen en memoria más de lo necesario?
- Cantidad de objetos: ¿Estamos creando demasiados objetos del mismo tipo?
- Hilos: ¿Hay demasiados hilos? ¿Existen bloqueos (deadlock, contention)?
- Llamadas a métodos: ¿Cuál es la profundidad de la pila? ¿Ocurre una recursión sin condición de salida?
2. Herramientas de perfilado
En el mundo Java hay varias herramientas clásicas (¡y gratuitas!) que permiten realizar perfilado. Consideremos las principales.
VisualVM
VisualVM es una herramienta gratuita que viene con el JDK (a partir de JDK 6). Permite:
- Conectarse a JVM locales y remotas.
- Ver memoria, hilos, CPU y la recolección de basura.
- Hacer un heap dump y analizarlo.
- Perfilar la aplicación por CPU y memoria.
¿Cómo iniciar VisualVM?
Suele estar en la carpeta del JDK: <ruta_al_JDK>/bin/jvisualvm
Lo ejecutas, eliges el proceso de la aplicación Java y ya puedes observar su vida como peces en un acuario (solo que aquí los «peces» son objetos e hilos).
JProfiler, YourKit
Son herramientas comerciales, pero muy potentes. Permiten:
- Perfilar memoria, CPU e hilos.
- Analizar «capturas» de memoria (heap dump).
- Buscar fugas, bloqueos largos y métodos lentos.
- Integrarse con IDE y CI/CD.
Para empezar basta con VisualVM, pero si «creces» hasta proyectos grandes, echa un vistazo a estas herramientas.
Java Flight Recorder (JFR)
JFR es una herramienta integrada en el JDK para recopilar eventos sobre el funcionamiento de la JVM. Es muy ligera, casi no afecta al rendimiento y permite recopilar información sobre:
- Tiempo de ejecución de métodos.
- Recolección de basura.
- Hilos, bloqueos y errores.
JFR es ideal para producción, cuando no se puede ralentizar la aplicación.
3. Práctica: perfilamos una aplicación sencilla
Vamos a crear una mini calculadora que sepa realizar cálculos largos y guardar el historial de operaciones (para tener bucles, colecciones y trabajo con memoria).
Ejemplo de código: «Calculadora lenta»
import java.util.ArrayList;
import java.util.List;
public class SlowCalculator {
private final List<String> history = new ArrayList<>();
public int add(int a, int b) {
simulateHeavyOperation();
int result = a + b;
history.add(a + " + " + b + " = " + result);
return result;
}
public int multiply(int a, int b) {
simulateHeavyOperation();
int result = a * b;
history.add(a + " * " + b + " = " + result);
return result;
}
public List<String> getHistory() {
return history;
}
// Simulación de una operación "pesada"
private void simulateHeavyOperation() {
for (int i = 0; i < 5_000_000; i++) {
Math.sqrt(i);
}
}
}
Y ahora, la clase principal:
public class Main {
public static void main(String[] args) {
SlowCalculator calc = new SlowCalculator();
for (int i = 0; i < 10; i++) {
calc.add(i, i * 2);
calc.multiply(i, i + 5);
}
System.out.println("Historial de operaciones:");
for (String entry : calc.getHistory()) {
System.out.println(entry);
}
}
}
¿Cómo perfilar esta aplicación?
- Compilar y ejecutar la aplicación.
- Abrir VisualVM (jvisualvm).
- Encontrar tu proceso (normalmente por el nombre de la clase Main).
- Ir a la pestaña CPU Profiler y pulsar Start.
- Dejar que el programa trabaje (o pulsar de nuevo el botón lento).
- Ver qué métodos consumen más tiempo.
Pregunta: ¿Qué método, en tu opinión, será el más «pesado»?
Respuesta: Por supuesto, simulateHeavyOperation() — ya que hace un bucle enorme de 5_000_000 iteraciones y llama a Math.sqrt.
4. Problemas de rendimiento típicos
Algoritmos lentos
La razón más común: una mala elección de algoritmo o estructura de datos. Por ejemplo, buscar en una lista en lugar de usar HashMap, u ordenar con «burbuja» en lugar de una ordenación rápida.
Ejemplo:
// Búsqueda lenta
for (String s : list) {
if (s.equals("target")) {
// encontrado
}
}
Es mejor usar Set o Map para búsqueda rápida.
Fugas de memoria
Una fuga de memoria es una situación en la que los objetos permanecen «vivos» (hay referencias a ellos) aunque ya no sean necesarios. Esto conduce a un aumento del consumo de memoria y, en última instancia, a un OutOfMemoryError.
public class MemoryLeakDemo {
private static List<byte[]> leakyList = new ArrayList<>();
public static void main(String[] args) {
while (true) {
leakyList.add(new byte[1_000_000]); // 1 MB
try { Thread.sleep(100); } catch (InterruptedException ignored) {}
}
}
}
¿Cómo encontrar fugas?
Haz un heap dump en VisualVM y mira qué objetos ocupan más memoria y por qué tienen referencias.
Creación excesiva de objetos
Si en un bucle creas muchos objetos del mismo tipo, no solo cargas al recolector de basura, sino que también puedes ralentizar la aplicación.
for (int i = 0; i < 1_000_000; i++) {
String s = new String("hello"); // ¡mal!
}
Es mejor usar constantes o el pool de cadenas (String pool).
Bloqueos de hilos
Si varios hilos compiten por el mismo recurso (por ejemplo, un método sincronizado), esto puede provocar bloqueos y caída del rendimiento.
public synchronized void doWork() {
// ...
}
¿Cómo detectarlos?
En la pestaña Threads de VisualVM se puede ver qué hilos están bloqueados y por qué.
5. Enfoques de optimización
Primero mide, luego optimiza
Regla principal de la optimización: No optimices lo que no ralentiza.
Primero perfila, encuentra los «hot spots» y solo entonces cambia el código. A veces la parte más «obvia» del código ocupa solo el 1 % del tiempo, y el verdadero «monstruo» está en alguna biblioteca o en un lugar inesperado.
Uso del perfilador para encontrar hot spots
Punto caliente (hot spot) es un método o un fragmento de código que ocupa la mayor parte del tiempo de ejecución de la aplicación.
En VisualVM esto se ve en la pestaña CPU Profiler:
- Ordena los métodos por tiempo de ejecución.
- Observa el stack trace: quién llama a quién.
- Recuerda que a veces el «culpable» no es tu código, sino una biblioteca o incluso el JDK.
Ejemplos de optimización
Ejemplo 1: Sustitución de algoritmo
Si ves que se gasta más tiempo en buscar en una lista, cambia List por HashSet.
Set<String> set = new HashSet<>(list);
if (set.contains("target")) {
// ¡rápido!
}
Ejemplo 2: Reducir el número de asignaciones
En lugar de crear nuevos objetos en un bucle, reutiliza o usa StringBuilder.
// Malo:
for (int i = 0; i < 10000; i++) {
String s = "Resultado: " + i;
}
// Mejor:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.setLength(0);
sb.append("Resultado: ").append(i);
String s = sb.toString();
}
Ejemplo 3: Caché
Si ves que un método pesado se llama muchas veces con los mismos parámetros, usa una caché.
Map<Integer, Double> sqrtCache = new HashMap<>();
public double cachedSqrt(int x) {
return sqrtCache.computeIfAbsent(x, Math::sqrt);
}
6. Demostración: aceleramos nuestra calculadora
Problema: simulateHeavyOperation() consume mucho tiempo
Paso 1. Perfilamos
En VisualVM se ve que casi todo el tiempo se va en Math.sqrt(i) dentro de un bucle de 5_000_000 iteraciones.
Paso 2. Optimizamos
Si solo es una simulación de carga, elimínala o reduce el número de iteraciones.
Si es lógica de negocio real, piensa si puedes:
- Almacenar en caché el resultado.
- Usar un algoritmo más rápido.
- Trasladar los cálculos a un hilo separado (si no es crítico para el usuario).
Ejemplo de optimización:
private void simulateHeavyOperation() {
// Antes 5_000_000, ahora 100_000
for (int i = 0; i < 100_000; i++) {
Math.sqrt(i);
}
}
Paso 3. Verificamos el resultado
Volvemos a ejecutar el perfilado: la aplicación funciona más rápido y la carga de CPU ha disminuido.
7. Visualización: proceso de optimización
flowchart TD
A[Inicio de la aplicación]
B["Perfilado (VisualVM)"]
C[Detección de cuellos de botella]
D[Optimización del código]
E[Perfilado de nuevo]
F[Mejora del rendimiento]
A --> B --> C --> D --> E --> F
E --> C
8. Errores típicos al perfilar y optimizar
Error n.º 1: Optimización «a ojo». Muy a menudo los desarrolladores empiezan a cambiar el código sin medir dónde está realmente el problema. El resultado: mucho trabajo y un beneficio mínimo.
Error n.º 2: Perfilado en condiciones «no reales». Hay que perfilar con datos y carga lo más cercanos posible a producción. Perfilar «en vacío» puede no revelar los problemas reales.
Error n.º 3: Ignorar las fugas de memoria. Si no miras el heap dump y no analizas las referencias, puedes tardar en notar que el programa «engorda» y pronto se caerá.
Error n.º 4: Perseguir optimizaciones microscópicas. No merece la pena pasar días acelerando código que ocupa el 0.1 % del tiempo de la aplicación. Primero, los principales cuellos de botella.
Error n.º 5: No tener en cuenta hilos y sincronización. En aplicaciones multihilo, los problemas de rendimiento a menudo están relacionados no con los algoritmos, sino con bloqueos y esperas (synchronized, contention).
Error n.º 6: Olvidar perfilar después de los cambios. Después de optimizar, verifica obligatoriamente el resultado: ¡a veces la «optimización» incluso puede ralentizar la ejecución!
GO TO FULL VERSION