CodeGym /Cursos /JAVA 25 SELF /Compresión y perfilado de la serialización

Compresión y perfilado de la serialización

JAVA 25 SELF
Nivel 45 , Lección 4
Disponible

1. Introducción: ¿por qué optimizar la serialización?

En las aplicaciones modernas, la serialización aparece por todas partes — desde protocolos de red hasta cachés distribuidos y el intercambio de datos entre servicios.

La velocidad y el tamaño de la serialización son críticos. Si la serialización es lenta, la aplicación «se ralentiza» al guardar o cargar datos, y la red o el disco quedan infrautilizados. Si los objetos resultan demasiado grandes, ocupan mucho espacio en disco, tardan más en transmitirse por la red y generan carga adicional en memoria y ancho de banda.

Las tareas típicas incluyen guardar un gran grafo de objetos en un archivo o caché, transmitir objetos por la red con latencia mínima y serializar/deserializar datos rápidamente en un sistema multihilo.

La conclusión es simple: optimizar la serialización no es una «función premium», sino una práctica necesaria para aplicaciones de alto rendimiento y escalables.

2. Optimización del tamaño de los datos serializados

Excluir datos innecesarios: palabra clave transient

Por defecto se serializan todos los campos del objeto, excepto los marcados como transient. Si un campo no necesita guardarse (por ejemplo, caché, datos temporales, referencias a servicios), márquelo como transient:

public class User implements Serializable {
    private String name;
    private transient String sessionToken; // no se serializará
}

Ventajas:

  • Menor tamaño del objeto serializado.
  • No habrá datos innecesarios/peligrosos en el archivo o en la red.

Serialización manual: interfaz Externalizable

Si necesita control total sobre qué y cómo se serializa, implemente la interfaz Externalizable y describa la serialización explícitamente (métodos writeExternal/readExternal):

public class Person implements Externalizable {
    private String name;
    private int age;
    private transient String secret;

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeUTF(name);
        out.writeInt(age);
        // no serializamos secret
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException {
        name = in.readUTF();
        age = in.readInt();
    }
}

Ventajas:

  • Solo se serializan los campos necesarios.
  • Se puede cambiar el formato de serialización sin perder compatibilidad.

Compresión: comprimir los datos serializados

Los objetos serializados suelen ocupar mucho espacio, especialmente si contienen cadenas repetidas y colecciones grandes. Puede reducirse el tamaño mediante compresión.

Ejemplo con GZIPOutputStream:

try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data.gz")))) {
    out.writeObject(bigObject);
}

Ejemplo con ZipOutputStream:

try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream("data.zip"))) {
    zip.putNextEntry(new ZipEntry("object"));
    ObjectOutputStream out = new ObjectOutputStream(zip);
    out.writeObject(bigObject);
    out.flush();
    zip.closeEntry();
}

Ventajas:

  • El tamaño del archivo puede reducirse varias veces (especialmente para grandes grafos de objetos).
  • Menor tráfico al transmitir por la red.

Desventajas:

  • La compresión/descompresión requiere tiempo adicional de CPU.

3. Optimización de la velocidad de serialización

Almacenamiento en búfer: para qué sirven BufferedOutputStream y BufferedInputStream

Problema:
Sin almacenamiento en búfer, cada llamada a write() o read() provoca un acceso del sistema al disco o a la red — ¡eso es muy lento!

Solución:
Utilice flujos con búfer:

try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
try (ObjectInputStream in = new ObjectInputStream(
         new BufferedInputStream(new FileInputStream("data.bin")))) {
    Object obj = in.readObject();
}

Ventajas:

  • Acelera significativamente la escritura/lectura de objetos grandes.
  • Reduce la cantidad de accesos a disco/red.

¿Cómo funciona?
El búfer acumula datos en memoria y los escribe en bloques, no byte a byte.

Copia rápida: FileChannel.transferTo

Si necesita copiar rápidamente un archivo serializado grande, use NIO y el método transferTo:

try (FileChannel src = new FileInputStream("data.bin").getChannel();
     FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
    src.transferTo(0, src.size(), dest);
}

Ventajas:

  • La copia se realiza a nivel del SO, evitando la amortiguación adicional en Java — muy rápido para archivos grandes.

4. Perfilado de la serialización

Medición simple del tiempo: System.nanoTime()

Para una evaluación rápida del rendimiento de la serialización puede utilizar System.nanoTime():

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data.bin")))) {
    out.writeObject(bigObject);
}
long end = System.nanoTime();
System.out.println("Tiempo de serialización: " + (end - start) / 1_000_000 + " ms");

Ventajas:

  • Simple y rápido.
  • Permite comparar variantes (con búfer, sin búfer, con compresión, etc.).

Desventajas:

  • Los resultados pueden fluctuar debido al GC y procesos en segundo plano.
  • No es adecuado para comparar diferencias microscópicas con precisión.

Perfilado preciso: JMH (Java Microbenchmark Harness)

Para una medición más precisa utilice JMH — una biblioteca específica para microbenchmarks.

Ejemplo de benchmark sencillo:

@Benchmark
public void serializeWithBuffer() throws Exception {
    try (ObjectOutputStream out = new ObjectOutputStream(
             new BufferedOutputStream(new FileOutputStream("data.bin")))) {
        out.writeObject(bigObject);
    }
}

Ventajas:

  • Tiene en cuenta el calentamiento de la JVM, el impacto del GC y el ruido del SO.
  • Proporciona resultados fiables y reproducibles.

Desventajas:

  • Requiere configuración y comprensión de la metodología de JMH.
  • Excesivo para comparaciones «a ojo».

5. Práctica: comparar tiempo y tamaño de la serialización

Hagamos un mini experimento: serialicemos un gran grafo de objetos (por ejemplo, una lista de 100_000 objetos con colecciones anidadas) de distintas formas y comparemos el tiempo y el tamaño del archivo.

Serialización sin almacenamiento en búfer ni compresión

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Sin buffer: " + (end - start) / 1_000_000 + " ms, tamaño: " +
    new File("data1.bin").length() + " bytes");

Serialización con almacenamiento en búfer

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new BufferedOutputStream(new FileOutputStream("data2.bin")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Con buffer: " + (end - start) / 1_000_000 + " ms, tamaño: " +
    new File("data2.bin").length() + " bytes");

Serialización con compresión (GZIP)

long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(
         new GZIPOutputStream(new FileOutputStream("data3.gz")))) {
    out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Con compresión: " + (end - start) / 1_000_000 + " ms, tamaño: " +
    new File("data3.gz").length() + " bytes");

Análisis de resultados

Al probar la serialización, se nota cuánto influyen el búfer y la compresión. Los archivos comprimidos suelen ser de 210 veces más pequeños (el factor exacto depende de la estructura de datos). Con búfer, la serialización va sensiblemente más rápida, y la compresión la ralentiza un poco, pero el ahorro de espacio suele compensarlo.

Conclusión: para grandes volúmenes de datos utilice obligatoriamente almacenamiento en búfer y, si el tamaño es crítico, añada compresión.

6. Errores típicos al optimizar la serialización

Error n.º 1: No usar almacenamiento en búfer — la serialización de objetos grandes se vuelve varias veces más lenta.

Error n.º 2: Serializar datos innecesarios o sensibles (por ejemplo, contraseñas, tokens temporales) — use siempre transient para esos campos.

Error n.º 3: Esperar que la compresión siempre acelera la serialización — en realidad la compresión reduce el tamaño, pero puede ralentizar un poco el proceso (especialmente en CPU débiles).

Error n.º 4: Medir el tiempo sin tener en cuenta el calentamiento de la JVM y el impacto del GC — para benchmarks precisos use JMH.

Error n.º 5: Comparar solo el tiempo o solo el tamaño — siempre observe ambos parámetros para elegir el equilibrio óptimo para su caso.

1
Cuestionario/control
Optimización de la serialización binaria, nivel 45, lección 4
No disponible
Optimización de la serialización binaria
Optimización de la serialización binaria
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION