Developpez.com

Plus de 14 000 cours et tutoriels en informatique professionnelle à consulter, à télécharger ou à visionner en vidéo.

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

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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 plate-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 :

 
Sélectionnez

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 disparue. 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 hards 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 :

 
Sélectionnez

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 douleurs sous l'affreuse révélation, on peut facilement provoquer des fuites mémoires en Java. Pour comprendre pourquoi cela peut arriver, rappelez-vous qu'un objet n'est libéré que si toutes ses hards 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 weaks 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 :

 
Sélectionnez

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 disparus 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érence que nous allons étudier est intitulée soft reference. A 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èmes 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 softs 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 occuperaient 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 :

 
Sélectionnez

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 softs 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.

Articles et tutoriels Java
L'essentiel de Java en une heure
L'API java.nio du JDK 1.4
Inversion de contrôle en Java
L'introspection
Le Java Community Process
Conception de tests unitaires avec JUnit
Les Strings se déchaînent
Présentation de SWT
La programmation réseau en Java avec les sockets
Du bon usage de l'héritage et de la composition
Les références et la gestion de la mémoire en Java
Constructeurs et méthodes exportées en Java
Les membres statiques, finaux et non immuables en Java
Les classes et objets immuables en Java
Comprendre et optimiser le Garbage Collector
Les principes de la programmation d'une interface graphique
Les opérateurs binaires en Java
Prenez le contrôle du bureau avec JDIC
Les Java Data Objects (JDO version 1.0.1)
La persistance des données avec Hibernate 2.1.8
Journalisation avec l'API Log4j
Java 5.0 et les types paramétrés
Les annotations de Java 5
Java 1.5 et les types paramétrés
Créer un moteur de recherche avec Lucene
Articles et tutoriels Swing
Threads et performance avec Swing
Rechercher avec style en utilisant Swing
Splash Screen avec Swing et Java3D
Drag & Drop avec style en utilisant Swing
Attendre avec style en utilisant Swing
Mixer Java3D et Swing
Articles et tutoriels Java Web
Redécouvrez le web avec Wicket
Cette création est mise à disposition sous un contrat Creative Commons (Paternité - Partage des Conditions Initiales à l'Identique).