1. Introduction : pourquoi optimiser la sérialisation ?
Dans les applications modernes, la sérialisation est partout — des protocoles réseau aux caches distribués et aux échanges de données entre services.
La vitesse et la taille de la sérialisation sont cruciales. Si la sérialisation est lente, l’application commence à « ralentir » lors de l’enregistrement ou du chargement des données, et le réseau ou le disque restent sous‑utilisés. Si les objets deviennent trop volumineux, ils occupent beaucoup d’espace disque, mettent plus de temps à être transférés sur le réseau et exercent une charge supplémentaire sur la mémoire et la bande passante.
Les tâches typiques incluent la sauvegarde d’un grand graphe d’objets dans un fichier ou un cache, le transfert d’objets sur le réseau avec une latence minimale et une sérialisation/désérialisation rapide des données dans un système multithread.
La conclusion est simple : l’optimisation de la sérialisation n’est pas une « fonctionnalité premium », mais une pratique nécessaire pour des applications performantes et évolutives.
2. Optimisation de la taille des données sérialisées
Exclure les données inutiles : le mot‑clé transient
Par défaut, tous les champs de l’objet sont sérialisés, sauf ceux marqués transient. Si un champ n’a pas à être sauvegagé (par exemple, un cache, des données temporaires, des références à des services), marquez‑le transient :
public class User implements Serializable {
private String name;
private transient String sessionToken; // ne sera pas sérialisé
}
Avantages :
- Taille de l’objet sérialisé plus petite.
- Pas de données superflues/dangereuses dans le fichier ou sur le réseau.
Sérialisation manuelle : interface Externalizable
Si vous avez besoin d’un contrôle total sur ce qui est sérialisé et comment, implémentez l’interface Externalizable et décrivez explicitement la sérialisation (méthodes 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);
// secret n'est pas sérialisé
}
@Override
public void readExternal(ObjectInput in) throws IOException {
name = in.readUTF();
age = in.readInt();
}
}
Avantages :
- Seuls les champs nécessaires sont sérialisés.
- On peut modifier le format de sérialisation sans perte de compatibilité.
Compression : compresser les données sérialisées
Les objets sérialisés occupent souvent beaucoup de place, surtout s’ils contiennent des chaînes répétitives et de grandes collections. On peut réduire la taille grâce à la compression.
Exemple avec GZIPOutputStream :
try (ObjectOutputStream out = new ObjectOutputStream(
new GZIPOutputStream(new FileOutputStream("data.gz")))) {
out.writeObject(bigObject);
}
Exemple avec 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();
}
Avantages :
- La taille du fichier peut diminuer fortement (surtout pour de grands graphes d’objets).
- Moins de trafic lors du transfert sur le réseau.
Inconvénients :
- La compression/décompression demande du temps supplémentaire (CPU).
3. Optimisation de la vitesse de sérialisation
Mise en mémoire tampon : pourquoi utiliser BufferedOutputStream et BufferedInputStream
Problème :
Sans mise en mémoire tampon, chaque appel à write() ou read() entraîne un accès système au disque ou au réseau — c’est très lent !
Solution :
Utilisez des flux tamponnés :
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();
}
Avantages :
- Accélère considérablement l’écriture/la lecture de gros objets.
- Réduit le nombre d’accès au disque/au réseau.
Comment cela fonctionne‑t‑il ?
Le tampon accumule les données en mémoire et les écrit par paquets, plutôt qu’octet par octet.
Copie rapide : FileChannel.transferTo
Si vous devez copier rapidement un gros fichier sérialisé, utilisez NIO et la méthode transferTo :
try (FileChannel src = new FileInputStream("data.bin").getChannel();
FileChannel dest = new FileOutputStream("copy.bin").getChannel()) {
src.transferTo(0, src.size(), dest);
}
Avantage :
- La copie s’effectue au niveau du système d’exploitation, en évitant un buffering inutile dans Java — très rapide pour les gros fichiers.
4. Profilage de la sérialisation
Mesure simple du temps : System.nanoTime()
Pour une évaluation rapide des performances de la sérialisation, vous pouvez utiliser 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("Temps de sérialisation : " + (end - start) / 1_000_000 + " ms");
Avantages :
- Simple et rapide.
- Permet de comparer différentes variantes (avec tampon, sans tampon, avec compression, etc.).
Inconvénients :
- Les résultats peuvent « varier » à cause du GC et des processus d’arrière‑plan.
- Inadapté pour comparer précisément de très petites différences.
Profilage précis : JMH (Java Microbenchmark Harness)
Pour des mesures plus précises, utilisez JMH — une bibliothèque dédiée aux microbenchmarks.
Exemple de benchmark simple :
@Benchmark
public void serializeWithBuffer() throws Exception {
try (ObjectOutputStream out = new ObjectOutputStream(
new BufferedOutputStream(new FileOutputStream("data.bin")))) {
out.writeObject(bigObject);
}
}
Avantages :
- Tient compte du warm‑up de la JVM, de l’influence du GC et du « bruit » du système d’exploitation.
- Fournit des résultats fiables et reproductibles.
Inconvénients :
- Nécessite une configuration et la compréhension de la méthodologie JMH.
- Excessif pour de simples comparaisons « à vue ».
5. Pratique : comparaison du temps et de la taille de sérialisation
Faisons une mini‑expérience : sérialisons un grand graphe d’objets (par exemple, une liste de 100_000 objets avec des collections imbriquées) de différentes manières et comparons le temps et la taille du fichier.
Sérialisation sans mise en mémoire tampon ni compression
long start = System.nanoTime();
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data1.bin"))) {
out.writeObject(bigList);
}
long end = System.nanoTime();
System.out.println("Sans tampon : " + (end - start) / 1_000_000 + " ms, taille : " +
new File("data1.bin").length() + " octets");
Sérialisation avec mise en mémoire tampon
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("Avec tampon : " + (end - start) / 1_000_000 + " ms, taille : " +
new File("data2.bin").length() + " octets");
Sérialisation avec compression (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("Avec compression : " + (end - start) / 1_000_000 + " ms, taille : " +
new File("data3.gz").length() + " octets");
Analyse des résultats
Lors des tests de sérialisation, on voit à quel point la mise en mémoire tampon et la compression influencent les résultats. Les fichiers compressés deviennent généralement de 2–10 fois plus petits (le facteur exact dépend de la structure des données). Avec un tampon, la sérialisation est nettement plus rapide, tandis que la compression ralentit légèrement le processus, mais le gain d’espace en vaut souvent la peine.
Conclusion : pour de gros volumes de données, utilisez impérativement la mise en mémoire tampon, et si la taille est critique — ajoutez la compression.
6. Erreurs courantes lors de l’optimisation de la sérialisation
Erreur n° 1 : Ne pas utiliser de tampon — la sérialisation de gros objets devient plusieurs fois plus lente.
Erreur n° 2 : Sérialiser des données inutiles ou sensibles (par exemple, mots de passe, jetons temporaires) — utilisez toujours transient pour ces champs.
Erreur n° 3 : S’attendre à ce que la compression accélère toujours la sérialisation — en réalité, la compression réduit la taille mais peut légèrement ralentir le processus (surtout sur des CPU modestes).
Erreur n° 4 : Mesurer le temps sans tenir compte du warm‑up de la JVM et de l’influence du GC — pour des benchmarks précis, utilisez JMH.
Erreur n° 5 : Ne comparer que le temps ou que la taille — regardez toujours les deux paramètres afin de choisir l’équilibre optimal pour votre cas.
GO TO FULL VERSION