Optimización y rendimiento
Principios de optimización
- Primero hazlo funcionar, luego hazlo rápido: No optimices prematuramente.
- Mide antes de optimizar: Usa profilers para identificar cuellos de botella.
- Optimiza lo que importa: El 80% del tiempo se gasta en el 20% del código.
Profiling
Herramientas
JProfiler: Profiler comercial completo.
VisualVM: Gratuito, incluido con el JDK.
jvisualvmJava Flight Recorder: Built-in desde Java 11.
java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MainAnalizar:
jcmd <pid> JFR.dump filename=recording.jfrOptimización de código
Evita crear objetos innecesarios
// ❌ Crea un nuevo String en cada iteraciónfor (int i = 0; i < 1000; i++) { String texto = "Iteración " + i;}
// ✅ Reutiliza StringBuilderStringBuilder sb = new StringBuilder();for (int i = 0; i < 1000; i++) { sb.setLength(0); sb.append("Iteración ").append(i);}Usa tipos primitivos cuando sea posible
// ❌ Autoboxing en bucles (crea objetos)List<Integer> lista = new ArrayList<>();for (int i = 0; i < 1000; i++) { lista.add(i); // Autoboxing: int → Integer}
// ✅ Array primitivo si no necesitas una Listint[] array = new int[1000];for (int i = 0; i < 1000; i++) { array[i] = i;}Lazy initialization
Solo crea objetos cuando se necesitan:
public class Servicio { private RecursoGrande recurso;
public RecursoGrande getRecurso() { if (recurso == null) { recurso = new RecursoGrande(); // Solo se crea cuando se usa } return recurso; }}Optimización de colecciones
Especifica capacidad inicial
// ❌ Crece dinámicamenteList<String> lista = new ArrayList<>();
// ✅ Evita realocacionesList<String> lista = new ArrayList<>(1000);Elige la colección correcta
// ❌ Buscar en lista: O(n)List<String> lista = new ArrayList<>();if (lista.contains("valor")) { }
// ✅ Buscar en set: O(1)Set<String> conjunto = new HashSet<>();if (conjunto.contains("valor")) { }Usa arrays para colecciones pequeñas y fijas
// ❌ Overhead de ArrayList para pocos elementosList<String> valores = new ArrayList<>(List.of("a", "b", "c"));
// ✅ Array directoString[] valores = {"a", "b", "c"};Optimización de Strings
StringBuilder en bucles
// ❌ Muy ineficienteString resultado = "";for (int i = 0; i < 1000; i++) { resultado += "valor" + i;}
// ✅ StringBuilderStringBuilder sb = new StringBuilder();for (int i = 0; i < 1000; i++) { sb.append("valor").append(i);}String resultado = sb.toString();String pool
// ✅ Reutiliza Strings del poolString a = "Hola";String b = "Hola"; // Misma referencia
// ❌ Crea nuevos objetosString c = new String("Hola"); // Nuevo objeto innecesarioOptimización de I/O
Buffering
// ❌ Lento: lee byte por bytetry (FileReader fr = new FileReader("archivo.txt")) { int c; while ((c = fr.read()) != -1) { // procesar }}
// ✅ Usa buffertry (BufferedReader br = new BufferedReader(new FileReader("archivo.txt"))) { String linea; while ((linea = br.readLine()) != null) { // procesar }}NIO para archivos grandes
// Files.readAllLines carga todo en memoriaList<String> lineas = Files.readAllLines(Path.of("archivo.txt"));
// ✅ Stream procesa línea por líneatry (Stream<String> stream = Files.lines(Path.of("archivo.txt"))) { stream.forEach(linea -> procesar(linea));}Optimización de bases de datos
Batch operations
// ❌ Un INSERT por vezfor (Usuario usuario : usuarios) { statement.executeUpdate("INSERT INTO usuarios VALUES (...)");}
// ✅ BatchPreparedStatement ps = connection.prepareStatement("INSERT INTO usuarios VALUES (?, ?)");for (Usuario usuario : usuarios) { ps.setString(1, usuario.getNombre()); ps.setString(2, usuario.getEmail()); ps.addBatch();}ps.executeBatch();Connection pooling
// ❌ Crear conexión para cada peticiónConnection conn = DriverManager.getConnection(url);// usar conexiónconn.close();
// ✅ Pool de conexiones (HikariCP, por ejemplo)HikariDataSource dataSource = new HikariDataSource();dataSource.setJdbcUrl(url);dataSource.setMaximumPoolSize(10);Connection conn = dataSource.getConnection(); // Reutiliza conexionesCaché
Caché en memoria
import com.github.benmanes.caffeine.cache.Cache;import com.github.benmanes.caffeine.cache.Caffeine;
Cache<String, Usuario> cache = Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build();
// Buscar en cache primeroUsuario usuario = cache.get(id, k -> buscarEnBaseDatos(id));Memoization
public class Fibonacci { private Map<Integer, Long> cache = new HashMap<>();
public long calcular(int n) { if (n <= 1) return n; return cache.computeIfAbsent(n, k -> calcular(n - 1) + calcular(n - 2)); }}Paralelismo
Parallel Streams
List<Integer> numeros = // lista grande
// Procesamiento paralelolong suma = numeros.parallelStream() .filter(n -> n % 2 == 0) .mapToLong(Integer::longValue) .sum();Cuidado: Solo úsalo si:
- El dataset es grande (>10,000 elementos)
- Las operaciones son costosas
- No hay estado compartido
CompletableFuture
Para operaciones asíncronas:
CompletableFuture<Usuario> futureUsuario = CompletableFuture.supplyAsync(() -> buscarUsuario(id));
CompletableFuture<Pedidos> futurePedidos = CompletableFuture.supplyAsync(() -> buscarPedidos(id));
// Esperar ambos resultadosUsuario usuario = futureUsuario.join();Pedidos pedidos = futurePedidos.join();Configuración de JVM
Ajustar heap
# Heap inicial y máximojava -Xms2g -Xmx4g MainElegir Garbage Collector
# G1 (por defecto)java -XX:+UseG1GC Main
# ZGC para baja latenciajava -XX:+UseZGC MainHabilitar optimizaciones
# Tiered compilation (por defecto)java -XX:+TieredCompilation Main
# Escape analysisjava -XX:+DoEscapeAnalysis MainBuenas prácticas
- Mide siempre: No asumas dónde está el problema.
- Optimiza algoritmos, no código: Un mejor algoritmo es más efectivo que micro-optimizaciones.
- Evita optimización prematura: Primero claridad, luego rendimiento.
- Considera el trade-off: A veces más memoria = más velocidad.
Próximo paso
Aprende a gestionar dependencias con Maven: Maven →