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 2–10 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.
GO TO FULL VERSION