Introducción: ¿por qué perfilar código Java?
\n\nDesarrollar en Java no solo consiste en escribir código correcto, sino también en crear aplicaciones rápidas y escalables. Incluso un sistema perfectamente diseñado puede ralentizarse debido a una sola línea no óptima. ¿Cómo encontrar ese "cuello de botella"? La respuesta es el perfilado.
\n\nEl perfilado es el proceso de recopilación de datos sobre el funcionamiento de una aplicación: tiempo de ejecución de métodos, uso de memoria, hilos y carga de CPU. Sin él, la optimización se convierte en una adivinanza. En este artículo, analizaremos ejemplos prácticos de perfilado de código Java, aprenderemos a interpretar los resultados y a encontrar cuellos de botella.
\n\nVeremos dos herramientas populares: VisualVM (gratuita, integrada en JDK) y JProfiler (comercial, pero potente). Todos los ejemplos serán con código real.
\n\nParte principal: práctica de perfilado
\n\n1. Configuración del entorno e inicio del perfilador
\n\nPara empezar, asegúrese de tener instalado JDK (8 o superior). VisualVM se incluye con JDK 8 y 9; para versiones más recientes, debe descargarse por separado desde el sitio oficial.
\n\nEscribamos un código "lento" simple para la prueba. Cree la clase SlowApp.java:
import java.util.ArrayList;\nimport java.util.List;\nimport java.util.Random;\n\npublic class SlowApp {\n public static void main(String[] args) throws InterruptedException {\n List<String> data = new ArrayList<>();\n Random random = new Random();\n\n // Simulación de carga lenta de datos\n for (int i = 0; i < 100_000; i++) {\n data.add(generateString(random));\n }\n\n // Procesamiento de datos\n long sum = 0;\n for (String s : data) {\n sum += processString(s);\n }\n\n System.out.println("Result: " + sum);\n Thread.sleep(10000); // Para tener tiempo de conectar el perfilador\n }\n\n private static String generateString(Random random) {\n StringBuilder sb = new StringBuilder(100);\n for (int i = 0; i < 100; i++) {\n sb.append((char) ('a' + random.nextInt(26)));\n }\n return sb.toString();\n }\n\n private static long processString(String s) {\n long result = 0;\n for (char c : s.toCharArray()) {\n result += c;\n // Retardo artificial\n for (int j = 0; j < 100; j++) {\n result += Math.sqrt(j);\n }\n }\n return result;\n }\n}\n\nEjecute la aplicación:
\njavac SlowApp.java\njava SlowApp\n\nMientras el programa se ejecuta (se dormirá durante 10 segundos), abra VisualVM. En el panel izquierdo, encuentre su proceso (SlowApp) y haga doble clic. Vaya a la pestaña Sampler o Profiler — verá estadísticas de CPU y memoria.
\n\n2. Análisis de CPU: encontrando el método más pesado
\n\nEn VisualVM, presione el botón CPU en la sección Sampler. Después de unos segundos, obtendrá una tabla con los métodos ordenados por tiempo de ejecución (Self Time).
\n\nEn nuestro ejemplo, verá que el método processString ocupa más del 80% del tiempo. Dentro de él, el bucle anidado Math.sqrt(j). Ese es el cuello de botella.
¿Cómo interpretar esto?
\n- \n
- Self Time — tiempo dedicado directamente a la ejecución del método (sin incluir llamadas a métodos hijos). \n
- Total Time — tiempo total, incluyendo todas las llamadas anidadas. \n
- Invocations — número de invocaciones del método. \n
Ejemplo de salida (simplificado):
\nMethod Self Time (ms) Invocations\nSlowApp.processString 8500 100000\nSlowApp.generateString 1200 100000\njava.lang.Math.sqrt 6000