Références faibles

images/blackbelt.png

Les techniques dites de "références faibles" permettent d’interagir avec le glaneur de mémoire (Garbage Collector) pour lui permettre de recupérer certaines références d’objets non-critiques, ou pour être averti si certains objets sont désalloués en mémoire (ou en passe de l'être).

Les références java.lang.ref.Reference sont des objets qui encapsulent une autre référence qui est gérée d’une manière particulière.

Seront abordés les References de type :

Dans ce chapitre nous utiliserons le terme générique "référence faible" pour recouvrir ces différentes catégories.

Problématiques des caches mémoire

Un des caractéristiques de Java est que la désallocaiton des objets en mémoire n’est pas sous le contrôle du programmeur. Une tâche de fond de basse priorité le glaneur de mémoire (Garbage Collector) s’occupe de rechercher les objets qui ne sont plus référencés, désalloue leur empreinte dans le tas et "tasse" la mémoire pour mieux la gérer. Pour permettre des évolutions des algorithmes de nettoyage mémoire la norme ne fixe que très peu de contraintes sur le comportement des garbage-collectors. Le programmeur sait qu’il peut "aider" une meilleure gestion de la mémoire en mettant explicitement certaines références à null et après certaines opérations de "nettoyage" il peut explicitement faire appel à System.gc() (dans ce dernier cas il ne lui est même pas garanti que le garbage-collector répondra à sa requête!)

Or il existe des situations où un certain degré de collaboration avec le glaneur de mémoire est souhaitable. Un de ces cas est lorsque l’on souhaite gérer des caches mémoire c’est à dire que pour des raisons d’optimisation on cherche à conserver des objets en mémoire mais de manière raisonnée et compatible avec les autres exigences de demande de place. Deux exemples:

  • Soit un programme qui affiche des images (représentation en mémoire couteuse); selon les interactions de l’utilisateur certaines images sont affichées souvent, d’autres non. Pour optimiser la gestion de la mémoire on souhaite adopter le comportement suivant: lorsqu’une image est demandée elle est chargée depuis une URL (opérations couteuse en temps!), les images sont conservées en mémoire mais lorsqu’on a besoin de place les images les moins récemment accédées seront eliminées: si l’utilisateur re-demande une image éliminée elle sera rechargée depuis son URL d’origine.
  • Soit un programme qui réalise un calcul très couteux en temps et en ressources. Des statistiques font apparaître qu’il n’est pas rare que ce calcul soit appelé avec les mêmes paramètres. On décide alors de mémoriser les résultats des précédents calculs: si le calcul a déjà été effectué on sera en mesure de rendre directment le résultat sans opérer un nouveau calcul couteux. La mémoire n'étant pas indéfiniment extensible à un moment donné il faudra se résoudre à abandonner tout ou partie des "stocks" de résultats.

Les objets références

La package java.lang.ref introduit des objets References. Ce sont des objets qui encapsulent une reférence vers un autre objet.

L’idée est que le programmeur puisse conserver une "reférence forte" vers un objet Reference, mais la référence contenue (referent object) fait l’objet d’une convention avec le garbage-collector. Ce dernier peut éventuellement récupérer l’objet contenu qui devient inaccessible au programmeur.

L’accès indirect à l’objet cible se fait par la méthode get() :

// les Reference sont des types paramétrés
monObjet = maReference.get() ;
if(monObjet == null) { // ah tiens! GC est passé!

Bien entendu on ne doit pas conserver de "référence forte" sur l’objet cible pointé par la référence faible (l’objet ne serait pas récupéré par le glaneur et la stratégie serait mise en echec).

Il existe différent types standard de "références faibles" en fonction de la stratégie attendue du glaneur de mémoire.

Il est possible d’associer à un objet Reference une file ReferenceQueue dans laquelle le glaneur mettra l’objet Reference après avoir recupéré l’objet cible. De cette manière le glaneur décide de signaler au programme son action sur la Reference : on peut donc mettre en place une surveillance de cette file (bloquante: méthode remove(), ou non bloquante: méthode poll()) et décider d’actions correspondantes (par exemple détruire la "référence forte" sur l’objet Reference lui-même).

SoftReference

Voici, par exemple, le code simplifié d’une Applet qui met en place la stratégie de cache d’images exposée précédemment.

// import divers
import java.lang.ref.* ;
public class SoftApplet extends Applet {
   SoftReference<Image> ref ;
   URL base ;
   String nom ;
   public void init() {
      base = getDocumentBase() ;
      nom = getParameter("image");//se proteger contre erreur ici!
      ref = new SoftReference<Image>(getImage(base,nom)) ;
   }

   public void paint(Graphics gr) {
      Image img = null ;
      if ( null == (img = ref.get())) {
         img = getImage(base, nom) ;
         ref = new SoftReference<Image>(img) ;
      }
      gr.drawImage(img, 50, 50, this) ;
   }
}

Les objets cibles reférencés au travers de références faibles de type SoftReference sont récupérées à la discrétion du glaneur de mémoire.

Celui-ci est toutefois encouragé à essayer de les éliminer en épargnant en priorité les objets les plus récemment créés ou accédés. En ce sens ces reférences sont "moins faibles" que les objets WeakReference.

On utlisera des SoftReferences pour suivre des stratégies basées sur l’historique des accès et d’une manière analogue on pourra utiliser des WeakReferences pour avoir des stratégies moins discriminantes (et probablement plus performantes).

On notera, dans le code d’illustration, qu’une fois qu’on a constaté que l’objet cible a disparu, il faut créer une nouvelle référence pour contenir un objet reconstitué.

WeakReference

Les reférences WeakReference contiennent des objets cibles qui sont récupérées librement par le glaneur de mémoire.

Souvent ces objets servent de "représentant canonique" pour un autre objet c’est à dire de clef caractéristique par laquelle on peut, potentiellement, retrouver un objet donné. Pour pousser cette analogie nous allons reprendre l’exemple du cache de calcul exposé précédemment et utiliser comme table de recherche un objet de type java.util.WeakHashMap.

L’objet caractéristique d’un calcul est sa combinaison de paramètres. Dans notre exemple le calcul "couteux" prendra deux paramètres entiers et nous allons décrire un objet représentant cette paire:

public class Parm2 {
   int x,y ;
   public Parm2 (int a, int b) {
      this.x = a; this.y = b ;
   }

   public boolean equals(Object autre) {
      if(! autre instanceof Parm2) return false ;
      Parm2 oth = (Parm2) autre ;
      return (x == oth.x) && ( y == oth.y) ;
   }

   public int hashCode() {
      return x ^ y ;
   }
}

On notera que, cet objet servant de clef pour une table de hash, il faut soigneusement définir la combinaison des méthodes equals() et hashCode().

Dans la WeakHashMap les clefs sont automatiquement générées comme reférences faibles et doivent être le seul point d’accès possibles pour les valeurs quelles représentent. Le glaneur de mémoire va pouvoir recupérer ces valeurs selon ses besoins.

public class CalculEnStock {
   WeakHashMap<Parm2,BigDecimal> dict = new WeakHashMap<Parm2,BigDecimal>() ;

   BigDecimal grosCalcul(int x, int y) {
      .....// gros calcul: (si vous n´êtes pas convaincu
      // essayez la fonction d'ackermann!)
      return °°°° ; // un BigDecimal
   }

   public BigDecimal  calcul(int x, int y) { // calcul stocké
      Parm2 parms = new Parm2(x,y) ;
      BigDecimal res =  dict.get(parms) ;
      if( res == null) {
         res = grosCalcul(x,y) ;
         dict.put(parms, res) ;
      }
      return res ;
   }
}

Ici le dictionnaire des résultats grossit au fur et à mesure des besoins, mais le glaneur a la faculté de mettre le holà à cette croissance et d'éliminer des résultats.

Cet exemple est de portée pédagogique : l'élimination des résultats stockés est radicale et peu discriminante (Il n’y a pas ici de stratégie particulière pour choisir les candidats à l'élimination). Une réalisation plus satisfaisante pourrait par ex. faire appel à java.util.LinkedHashMap utilisant un paramètre spécifiant un ordre basé sur l’accès et une taille limite controlée par removeEldestEntry.

Opérations liées à l’abandon d’un objet, finaliseurs, PhantomReferences

Avant de récupérer l’empreinte mémoire d’un objet le garbage-collector appelait sa méthode finalize() (redéfinie quand on voulait comportement associé à cette phase de la vie de l’objet).

L’utilisation de finalize() est obsolete (deprecated) : l’appel survient à des moments imprévus, peut être exécuté dans des threads différents et il n’est pas garanti que tous les objets aient été récupérés au moment de l’arrêt de la machine virtuelle (la méthode System.runFinalizersOnExit() a été aussi rendue obsolete du fait de ces risques).

Les PhantomReferences permettent d’associer, de manière plus souple, des méthodes à cette phase de fin de vie .

Exemple. Soit la définition d’une interface:

public interface DerniereVolonte {
   public void adieu() ;
}

et maintenant la définition (pour simplifier les codes nous n’avons pas utilisé de paramètre-type).

public class PresqueFantome extends PhantomReference {
   private DerniereVolonte vol ;
   public PresqueFantome(
         DerniereVolonte vl, Object obj, ReferenceQueue queue){
      super(obj,queue) ;
      vol = vl ;
   }

   public void preMortem() {
      vol.adieu() ; // se proteger des exceptions
      clear() ;
   }
}

Les PhantomReferences sont obligatoirement associées à une ReferenceQueue. Quand le glaneur de mémoire va recupérer l’objet cible de la reférence, son finaliseur est appelé et la référence est mise dans la file. A partir de ce moment, bien que l’empreinte de l’objet n’aie pas été recupérée, il est impossible d’obtenir l’objet (de toute façon get() rend toujours null sur une telle référence) - par contre au cours du traitement de la reférence il faudra explicitement annuler la référence vers l’objet cible (clear()).

Utilisations possibles de nos reférence PresqueFantomes: admettons qu’elles aient été toutes construites avec la file suivante :

final ReferenceQueue ref = new ReferenceQueue() ;

Recupération par un thread en cours d’exécution:

   Thread th = new Thread("fantomes") {
      {setDaemon(true) ;}

      public void run() {
         while(true) {
            try {
               Reference curRef = ref.remove();// bloquant!
               ((PresqueFantome)curRef).preMortem() ;
               // suite du ménage .....
            } catch(Exception exc) {
               curlogger.warning(exc.toString()) ;
            }
         }
      }
   } ;
   th.start() ;

Notons que dans dans ce cas on ne déclenche la méthode associée au décès que si le glaneur est passé pour l’objet cible. Il peut être utile de prévoir de faire quelque chose pour les autres.

Par ailleurs pour déclencher des codes dont l’exécution serait garantie il faudrait plutot utiliser des shutdownHooks (voir java.lang.Runtime méthode addShutdownHook).