Un café «Java» ... fort ou léger ?
Au hasard du web, je tombe sur un vieil article parlant de référence faible (weak reference) et disant (je résume très fort) que très peu de programmeurs Java savent ce que sont les références faibles ... et comme je ne savais pas non plus, je me renseigne ;-)
Some time ago I was interviewing candidates for a Senior Java Engineer position. Among the many questions I asked was "What can you tell me about weak references?" I wasn't expecting a detailed technical treatise on the subject. I would probably have been satisfied with "Umm... don't they have something to do with garbage collection?" I was instead surprised to find that out of twenty-odd engineers, all of whom had at least five years of Java experience and good qualifications, only two of them even knew that weak references existed, and only one of those two had actual useful knowledge about them.
Extrait de Ethan Nicolas
En lisant l'article je pense à un projet Java que codent mes étudiants où il pourrait être intéressant de mettre en cache certaines images afin de ne pas les relire sur le disque (ce qui est coûteux) trop souvent. Mais comment faire ça ?
Tâchons de répondre à ces deux questions ...
- Références fortes (strong reference)
- Références faibles (weak reference)
- Références douces (soft reference)
- Comment mettre en place un «cache» en Java ?
Références fortes (strong reference)
Une référence forte en Java est une référence, bien connue, créée comme suit
MyObject mo = new MyObject();
Tant que ma variable conserve une référence vers l'objet, celui-ci ne sera pas éligible par le garbage collector [1] (Wikipedia). L'objet reste donc toujours en mémoire.
Gérer sa mémoire en Java consiste simplement à gérer ses variables. En Java j'ai assez peu de chances d'avoir des problèmes de fuite mémoire (memory leak) ou de pertes d'objets (damned ! J'ai détruit un objet dont j'avais encore besoin). Ceci est d'ailleurs vrai pour tous les langages utilisant un garbage collector.
Si je voulais gérer un cache pour des images en utilisant les références fortes (habituelles) je devrais
- choisir une taille pour mon cache
- sauvegarder des images dans un container quelconque (un
HashMap
par exemple) - décider de supprimer certaines images (lesquelles ?) lorsque je veux en ajouter d'autres
- choisir une politique de conservation dans le cache (le plus souvent utilisé, les x derniers, ...)
... tout ceci me semble assez «manuel» (et donc fastidieux).
Références faibles (weak reference)
Java définit la notion de référence faible (weak reference), dans sa javadoc de la classe WeakReference
, comme
Définition Une référence faible est une référence qui n'est pas suffisament forte pour forcer un objet à rester en mémoire suite au passage du garbage collector
Si l'on suppose l'existence d'une classe MyObject
représentant un «objet quelconque», on pourra l'utiliser comme suit[2]:
WeakReference<MyObject> wr = new WeakReference<>( new MyObject(//params//)); wr.get();
L'object sera disponible un certain temps et si le garbage collector estime qu'il peut le récupérer, il le fera. Dans ce cas, la méthode get
retournera null
.
Il doit être clair, qu'il ne doit plus y avoir de référence forte vers cet objet
import java.lang.ref.WeakReference; public class TestWeakReference { public static void main ( String[] args ) { WeakReference<MyObject> wr = new WeakReference<>( new MyObject(5, "description")); // Il faut gérer les références fortes // MyObject mo = wr.get(); for (int i=0; i<10; i++) { System.out.println(wr.get()); if (i==5) System.gc(); } } }
On obtient les résultats suivant suivant que l'on décommente ou pas la ligne.
On peut mettre en œuvre un exemple un peu plus parlant. Je vais utiliser une classe permettant de réserver un certain nombre de bytes en mémoire. Nous allons utiliser une classe MemoryBlock
(l'idée provient de Alexandre Pereira Calsavara ) telle que
public class MemoryBlock { private int id; private int size; private byte[] block; public MemoryBlock( int id, int size ) { this.id = id; this.size = size; block = new byte[size]; System.out.println("MemoryBlock created: " + this); } @Override public String toString() { return "{id="+id+",size="+size+"}"; } @Override // Méthode appelée lorsque l'on libère l'objet protected void finalize() { System.out.println( "MemoryBlock finalized: "+this ); } }
Je vais maintenant créer des blocs de plus en plus grands jusqu'à ce que le garbage collector décide de libérer de l'espace mémoire.
import java.lang.ref.WeakReference; import java.lang.ref.Reference; import java.util.List; import java.util.ArrayList; public class TestWeakReferenceMemoryBlock { public static void main ( String[] args ) { List<Reference<MemoryBlock>> list = new ArrayList<>(); int size = 65536; int id = 0; while (true) { list.add(new WeakReference<>( new MemoryBlock(id++, size))); size *= 2; display(list); System.gc(); } } public static void display(List<Reference<MemoryBlock>> list){ for (Reference<MemoryBlock> sr : list) { System.out.println("" + sr.get()); } } }
Remarque
Ôter l'appel explicite au garbage collector induit une OutOfMemoryError
:-(
Références douces soft reference
La différence entre une référence douce et une référence faible réside dans le fait que la libération de l'espace mémoire est maintenant laissé à la discrétion du garbage collector en fonction de l'espace mémoire dont il dispose.
Si je %s/WeakReference/SoftReference/
dans l'exemple précédent, je m'attend à ce que la libération des objets se fasse plus tard.
En lisant la littérature, je vois que l'objet ReferenceQueue
me permet d'obtenir une liste des objets candidats à l'exécution par le garbage collector. Les objets de cette liste peuvent donc être retirés de ma liste d'objets.
import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.lang.ref.ReferenceQueue; import java.lang.ref.Reference; import java.util.List; import java.util.ArrayList; public class TestSoftReferenceQueue { public static void main ( String[] args ) { ReferenceQueue<MemoryBlock> queue = new ReferenceQueue<>(); List<Reference<MemoryBlock>> list = new ArrayList<>(); int size = 65536; int id = 0; Reference<MemoryBlock> reference; while (true) { reference = new SoftReference<>( new MemoryBlock(id++, size), queue); list.add(reference); size *= 2; display(list); System.gc(); Reference<MemoryBlock> sr; while ((sr=queue.poll()) != null) { list.remove(sr); } } }
Remarque
Cet exemple n'est pas concluant. J'obtiens une OutOfMemoryError
et aucun objet n'est libéré ... reste à voir si je peux mettre en œuvre ce cache dont on parle et si l'objet WeakHashMap
répond à mes attentes. Suspense !
Comment mettre en place un «cache» en Java ?
L'objet WeakHashMap
est une table (map) dont les clés sont des références faibles (weak références) et telle que lorsque la référence n'est plus disponible, l'entrée est supprimée du map.
Hash table based implementation of the Map interface, with weak keys. An entry in a WeakHashMap will automatically be removed when its key is no longer in ordinary use. More precisely, the presence of a mapping for a given key will not prevent the key from being discarded by the garbage collector, that is, made finalizable, finalized, and then reclaimed. When a key has been discarded its entry is effectively removed from the map, so this class behaves somewhat differently from other Map implementations.
Extrait de l'API Java
Avec cet objet, implémenter un cache est très simple. Je ne dois pas m'inquiéter de la taille de mon cache, c'est Java qui va la gérer en fonction de la mémoire dont il dispose. Lorsque j'ai besoin d'un objet,
- je regarde s'il est présent dans le map,
- si oui, je l'utilise
- si non, je le charge et l'ajoute dans le map pour plus tard
Soit le code suivant permettant de tester l'utilisation de WeakHashMap
pour créer facilement un cache. Je crée des blocs que j'ajoute dans le cache et, à chaque création, j'affiche le contenu du chache histoire de voir des blocs sont supprimés du cache.
import java.util.WeakHashMap; public class TestWeakHashMap2 { private static WeakHashMap<String,MemoryBlock> cache = new WeakHashMap<>(); private static int n = 0; public static void main ( String[] args ) { int size = 6*65536; while (n<50) { cache.put(""+n++, new MemoryBlock(n, size)); System.out.printf("Keys in cache: "); for (String s : cache.keySet()) { System.out.printf(" %s ", s); } System.out.printf("\n"); } } }
Et l'on constate qu'à partir d'un certain moment, des blocs disparaissent du cache (et ceci sans faire passer explicitement le garbage collector). Le test est donc assez concluant.
Hormis les deux remarques concernant le garbage collector et les références souples (soft reference), les tests sont assez concluants.
Si l'on se réfère à l'introduction, nous pouvons maintenant nous présenter sans soucis à un entretien d'embauche et répondre que l'on sait ce que sont les références faibles (weak reference)[3]
Enjoy !