Références et Gestion de la mémoire

Romain Guy (Gfx)

- Titre : Références et Gestion de la mémoire

- Auteur : Romain Guy (Gfx)

- Parution : 8 août 2006

- Mise à jour : 10 novembre 2019

- Licence : Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).

Sommaire

- I. Références et Gestion de la mémoire

I - Références et Gestion de la mémoire

Java simplifie grandement la gestion des données par rapport à des langages comme le C ou le C++ en ne permettant au développeur de ne manipuler que des références. La libération des objets est en outre réalisée par le garbage collector. Ces particularités ont de nombreux avantages, mais rendent très difficile une gestion fine de la mémoire. Elles peuvent également introduire des problèmes importants dans vos programmes.

Les plates-formes Java SE et Java EE proposent trois sortes de références qu'il est important de connaître si vous désirez écrire des programmes de qualité. Les références connues de tous et utilisées par défaut par le langage Java sont les « hard references ». Voici un exemple de création et d'utilisation d'une telle référence :

List images = new ArrayList();
images.add(ImageIO.read(new File("logo.png")));

L'ArrayList référencée par images sera libérée par le garbage collector lorsque toutes ses hard references auront disparu. Pour détruire une référence, il suffit de lui affecter la valeur null ou d'attendre que le flot d'exécution la fasse disparaître. Par exemple, si nous plaçons les lignes précédentes dans une méthode, la référence sera détruite lorsque la JVM terminera l'exécution de la méthode. Les hards references suffisent dans la plupart des cas et ont l'avantage d'être non seulement très simples à comprendre, mais également à manipuler.

Malgré cela, les hard references peuvent se révéler dangereuses dans des cas bien particuliers. L'écriture d'une architecture d'écouteurs/événements est une des situations les plus courantes d'une mauvaise utilisation de ces références. Considérons l'exemple suivant qui définit un système d'écouteurs/événements simple :

public interface BeerListener {
public void beerPoured(BeerEvent e);
}

public class Barman {
private final List listeners = new LinkedList();

public void addBeerListener(BeerListener listener) {
listeners.add(listener);
}

public void removeBeerListener(BeerListener listener) {
listeners.remove(listener);
}

private void fireBeerPouredEvent() {
BeerEvent e = new BeerEvent(BeerEvent.Type.POURED);
for (BeerListener listener : listeners) {
listener.beerPoured(e);
}
}

public void pourBeer() {
// get a glass
// pour beer in the glass
fireBeerPouredEvent();
}
}

Cet exemple ne reflète en rien la meilleure manière d'écrire un système d'écouteurs/événements, mais nous reviendrons sur ce sujet plus tard. Cet exemple contient un problème beaucoup plus grave que la simple propreté du code : une fuite mémoire. J'imagine déjà de nombreux sourcils se froncer et des cerveaux se plisser de douleur sous l'affreuse révélation, on peut facilement provoquer des fuites mémoire en Java. Pour comprendre pourquoi cela peut arriver, rappelez-vous qu'un objet n'est libéré que si toutes ses hard references sont détruites. Dans notre exemple nous maintenons une liste de hard references vers les écouteurs qui ne sont donc jamais libérés s'ils ne sont pas retirés de la liste avec un appel à removeBeerListener(). Cette situation est malheureusement très courante en particulier avec Swing et les interfaces graphiques. Bien qu'il est possible, par contrat, de forcer les clients à invoquer la méthode de suppression des références il existe une solution bien plus efficace et plus adaptée à l'esprit du langage Java, les weak references.

Les weak references n'existent pas au niveau du langage et nécessitent donc l'emploi d'une classe particulière intitulée java.lang.ref.WeakReference. Ces références se distinguent des hard references par le fait que l'objet auquel elles font référence est automatiquement libéré si toutes les références à ce dernier sont des weak references. Voici un exemple d'utilisation d'une weak reference :

private WeakReference listener;

public void setBeerListener(BeerListener listener) {
this.listener = new WeakReference(listener);
}

public BeerListener getBeerListener() {
return listener.get();
}

private void fireBeerPouredEvent() {
if (listener == null) {
return;
}

BeerEvent e = new BeerEvent(BeerEvent.Type.POURED);
BeerListener listener = this.listener.get();
if (listener != null) {
listener.beerPoured(e);
}
}

La méthode fireBeerPouredEvent() est le point le plus intéressant de cet exemple. Une weak reference encapsule l'objet à référencer que nous pouvons récupérer en invoquant la méthode get(). Puisque les objets ainsi référencés peuvent être libérés à tout moment par le garbage collector, nous devons vérifier qu'ils n'ont pas disparu avant de nous en servir. Cela force également le programmeur à obtenir une hard reference vers l'objet pour empêcher le garbage collector de le supprimer alors que nous désirons nous en servir. L'exemple précédent a volontairement été simplifié à un seul et unique écouteur pour bien comprendre l'utilisation de WeakReference. En pratique, une liste de weak references peut aisément être maintenue en utilisant la collection java.util.WeakHashMap. Il ne s'agit pas d'une liste à proprement parler, mais son usage peut être détourné à cet effet. Je vous invite à consulter sa documentation pour plus de détails.

La troisième sorte de références que nous allons étudier est intitulée soft reference. À l'instar des weak references, cette référence n'existe pas dans le langage Java. Nous devons donc utiliser la classe java.lang.ref.SoftReference. Les soft references sont très utilisées pour gérer des grands ensembles de données en mémoire sans provoquer d'utilisation abusive des ressources système ni obliger l'utilisateur à augmenter la taille du heap de la JVM. Ces références sont donc utilisées pour créer des caches. Un objet référencé uniquement par des soft references est libéré si la JVM a besoin de mémoire (par exemple juste avant qu'une OutOfMemoryError ne survienne). De fait, vous devez utiliser des soft references pour référencer des données que vous pouvez aisément recharger.

Imaginez une application similaire à Google Maps qui affiche une carte du monde découpée en tiles (carrés de taille fixe). Pour accélérer le déplacement sur la carte, les tiles sont conservés en mémoire. Conserver tous les tiles serait irréaliste puisque l'ensemble complet des données occuperait des Go en RAM et conduirait très rapidement à une occupation complète de la mémoire. Pour pallier ce problème il suffit d'utiliser des soft references :

public class Tile {
private double lat = ...;
private double long = ...;

private SoftReference image = new SoftReference(null);

public Image getImage() {
Image tileImage = image.get();
if (tileImage == null) {
startAsynchronousLoading();
}
}

// loading and stuff
}

Comme vous le constatez, l'API de SoftReference est en tout point semblable à celle de WeakReference. Avec le code ci-dessus, l'application se débarrasse des encombrantes images représentant chaque morceau de la carte dès que la JVM a besoin de libérer de la mémoire. L'application continue cependant à fonctionner puisque les données remontent en mémoire lorsqu'un accès est réalisé. Les soft references sont généralement combinées aux hard references pour maintenir constamment en mémoire les données les plus susceptibles d'être requises. Dans le cas d'une carte du monde nous garderions des hard references vers tous les tiles situés dans la vue courante pour garantir un affichage rapide. Je montrerai très probablement un exemple concret d'utilisation des soft references après JavaOne. Nous avons récemment utilisé cette technique pour une démo gourmande en mémoire.

L'API Java contient une quatrième sorte de référence, les PhantomReference dont l'utilité est beaucoup plus limitée. Vous pouvez consulter la documentation officielle pour en savoir plus à leur sujet.