Author: Christian.Queinnec@lip6.fr

## Allocation et récupération mémoire

La plupart des langages permettent d'allouer dynamiquement de la mémoire. On peut citer C, Pascal, Lisp, ML, SmallTalk, C++, Java, ADA.

### Allocation explicite

On distingue deux types d'allocation :

- une allocation simple qui réserve une zone mémoire d'une certaine taille sans se préoccuper de son contenu ;
- une allocation qui joint à la réservation d'un espace mémoire une initialisation pour créer des valeurs.

Le premier cas est illustré par les fonction `new` de Pascal ou `malloc` de C. Elles retournent un pointeur sur une zone mémoire (i.e. son adresse), on peut alors accéder à cette zone pour consulter ou modifier la valeur qu'elle contient. Le deuxième cas correspond aux fonctions de construction de valeurs d'Objective CAML ou celle des langages à objets. Les instances de classes des langages objets sont construites en passant à un opérateur `new`, la fonction de construction particulière à la classe qui, elle-même attend, en général, un certains nombre de paramètres d'initialisation. Pour les langages fonctionnels, les fonctions de constructions sont immédiatement utilisées dès que l'on définit une valeur structurée (un couple, une liste, un enregistrement, un vecteur, une fermeture).
Examinons la construction de valeurs Objective CAML sur un exemple. Leur représentation mémoire est illustrée par le schéma de la figure 9.1.

> Figure 9.1 : représentation mémoire de valeurs

    # let u = let l = ['c'; 'a'; 'm'] in List.tl l ;;
    val u : char list = ['a'; 'm']
    # let v = let r = ( ['z'] , u )
             in match r with p -> (fst p) @ (snd p) ;;
    val v : char list = ['z'; 'a'; 'm']

On a représenté un élément de liste par un doublet de deux mots, le premier contenant un caractère et le second un pointeur vers l'élément suivant de la liste. La représentation réelle est légèrement différente. Elle est détaillée dans le chapitre sur l'interface avec C (voir page ??).
La première phrase construit une valeur, nommée `l`, en allouant une cellule (constructeur ::) pour chaque élément de la liste `[``'c'`;`'a'`;`'m'``]`. La déclaration globale `u` correspond à la queue de `l`. Il y a ici un partage entre `l` et `u`, c'est-à-dire entre l'argument et le résultat de l'application de List.tl. Seule la déclaration `u` est connue après l'évaluation de cette première phrase (`l` cesse d'exister).
La deuxième phrase construit une liste à un seul élément, puis un couple nommé `r` contenant cette liste et la liste `u`. Ce couple est filtré et renommé `p` par le filtrage. Ensuite, le premier élément de `p` est concaténé avec son second élément, ce qui crée une valeur `[``'z'`;`'a'`;`'m'``]` liée à l'identificateur global `v`. On observe que le résultat de `snd` (la liste `[``'a'`;`'m'``]`) est partagé avec son argument `p` alors que le résultat de `fst` (le caractère `'z'`) est copié.
Dans tous les cas l'allocation mémoire est explicite. C'est-à-dire qu'elle est demandée par le programmeur (comme instruction ou expression du langage).

### Remarque

L'allocation mémoire conserve une information sur la taille de l'objet alloué dans le but de pouvoir le libérer par la suite.

### Récupération explicite

Les langages dont la récupération mémoire est explicite possèdent un opérateur de libération (`free` en C ou `dispose` en Pascal) qui reçoit l'adresse (un pointeur) de la zone à désallouer. En utilisant les informations conservées au moment de l'allocation, le programme libère cette zone et pourra la réutiliser par la suite.
L'allocation dynamique est en général utilisée pour manipuler des données structurées évolutives telles les listes, les arbres, etc. Libérer l'espace occupé par de telles données ne se fait pas en << un coup de cuillère à pot >> mais réclame une fonction de parcours de la donnée. On appelle de telles fonctions des destructeurs.
Si la définition correcte des fonctions de destruction ne pose pas trop de difficulté, leur usage est, en revanche, plus délicat. En effet, pour libérer l'espace occupée par une structure, il faut en parcourir les pointeurs et appliquer l'opérateur de libération mémoire du langage. Laisser la libération de la mémoire à la charge du programmeur présente l'avantage que ce dernier est sûr des actions effectuées. Néanmoins une mauvaise utilisation de ces opérateurs peut provoquer une erreur à l'exécution du programme. Les principaux dangers de la récupération explicite sont :

- les pointeurs << fantômes >> (dandling pointers) : Une zone mémoire a été libérée alors qu'il subsiste des pointeurs qui référencent toujours cette zone. Si celle-ci est réutilisée, l'accès à cette zone par ces pointeurs risque d'être incohérent.
- les zones mémoire inaccessibles (cas de << fuite >> ou leak) : Une zone mémoire toujours allouée qui n'est plus référencée par aucun pointeur. Il n'y a plus de possibilités de la libérer. Il y a une perte sèche de mémoire.

C'est toute la difficulté de la récupération explicite de mémoire que de connaître la durée de vie de l'ensemble des valeurs d'un programme.

### Récupération implicite

Les langages avec récupération implicite ne possèdent pas d'opérateurs de libération mémoire. Il n'est pas possible pour le programmeur de décréter la disparition d'une valeur allouée. Néanmoins, un mécanisme de récupération automatique est déclenché quand une valeur n'est plus référencée, ou lors d'un échec de l'allocation, c'est-à-dire quand le tas est plein.
Un algorithme de récupération automatique de mémoire est en quelque sorte un destructeur global. Ce caractère rend sans doute plus difficile sa conception et son implantation que le seraient celles d'un destructeur dédié à une structure de données particulière. Mais, une fois cette difficulté surmontée, la fonction de récupération obtenue accroît considérablement la sûreté de la gestion mémoire. En particulier le risque de pointeurs fantômes disparaît.
D'autre part, une fonction de récupération automatique de mémoire peut apporter de bonnes propriétés sur le tas :

- compaction : toute la mémoire récupérée tient en un seul bloc, évitant ainsi la granularité du tas et permettant d'allouer des objets de la taille du tas libre ;
- localisation : les différents composants d'une même valeur sont proches du point de vue adressage mémoire, ce qui permet lors de leur parcours de rester dans les mêmes pages mémoire, évitant ainsi de sortir de la mémoire-cache.

Les choix de conception d'un GC doivent tenir compte d'un certain nombre de critères et de contraintes :

- facteur de récupération : quel pourcentage de la mémoire inutilisée est disponible ?
- fragmentation de la mémoire : peut-on allouer un bloc de la taille de la mémoire libre ;
- ralentissement de l'allocation et de la récupération ;
- compilation : la représentation des valeurs est-elle totalement libre ?

En pratique, le critère de sûreté reste primordial, et les GC trouvent un compromis concernant les autres contraintes.