Gestion de la mémoire

Introduction

Les langages de bas niveau, tels que C, possèdent des primitives permettant de gérer la mémoire : malloc() et free() par exemple. En revanche, lorsqu'on utilise JavaScript, la mémoire est allouée lors de la création des objets puis libérée « automatiquement » lorsque ceux-ci ne sont plus utilisés. Cette libération automatique est appelée garbage collection en anglais ou ramasse-miettes. Le fait que ce processus soit automatique est souvent source de confusion et donne parfois l'impression que JavaScript (ou d'autres langages de haut niveau) ne permet pas de gérer la mémoire : nous allons voir que ce n'est pas le cas.

Le cycle de vie de la mémoire

Quel que soit le langage de programmation, le cycle de vie de la mémoire ressemblera à :

  1. Allouer la mémoire dont on a besoin
  2. Utiliser cette mémoire allouée (lecture, écriture)
  3. Libérer la mémoire allouée lorsqu'on n'en a plus besoin

Les deux premiers points sont explicites, au niveau du code, pour tous les langages de programmation. Le troisième point est explicite pour les langages de bas niveau mais souvent implicite pour les langages de haut niveau tels que JavaScript.

Allocation de la mémoire en JavaScript

Initialisation des valeurs

Afin de simplifier l'écriture de code, JavaScript alloue la mémoire lors de la déclaration des variables :

var n = 123; // alloue de la mémoire pour un nombre
var s = "azerty"; // alloue de la mémoire pour une chaîne de caractères

var o = {
  a: 1,
  b: null
}; // alloue de la mémoire pour un objet et les valeurs qu'il contient

var a = [1, null, "abra"]; // alloue de la mémoire pour un tableau et les valeurs qu'il contient

function f(a){
  return a + 2;
} // alloue de la mémoire pour une fonction 
// une fonction est un objet qui peut être appelé

// les expressions de fonction allouent aussi de la mémoire
unÉlément.addEventListener('click', function(){
  unÉlément.style.backgroundColor = 'blue';
}, false);

Allocation par appels de fonctions

Certains appels de fonctions entraînent l'allocation mémoire d'un objet.

// Alloue la mémoire pour un objet date
var d = new Date();
// Alloue de la mémoire pour un objet représentant un élément du DOM
var e = document.createElement('div');

Certaines méthodes allouent de la mémoire pour des nouveaux objets ou de nouvelles valeurs.

var s = "azerty";
var s2 = s.substr(0, 3); // s2 est une nouvelle chaîne de caractères
// Les chaînes étant immuables, JavaScript peut choisir 
// de ne pas allouer de mémoire mais seulement 
// de stocker l'intervalle [0, 3].

var a = ["ouais ouais", "nan nan"];
var a2 = ["génération", "nan nan"];
var a3 = a.concat(a2);
// nouveau tableau de 4 éléments 
// (résultat de la concaténation de a et a2)

Utilisation des variables

Utiliser des variables revient à lire et écrire la mémoire allouée. Cela peut être effectué lorsqu'on lit ou modifie la valeur d'une variable ou d'une propriété d'un objet ou encore lorsqu'on passe un argument à une fonction.

Libérer la mémoire qui n'est plus nécessaire

La plupart des problèmes concernant la gestion de la mémoire surviennent à cet endroit. Le plus difficile est de savoir « quand » la mémoire allouée n'est plus utilisée. Il faut donc que le développeur détermine quelle partie de la mémoire n'est plus utilisée à tel endroit du code et la libère.

Les interpréteurs des langages de haut niveau intègrent un composant logiciel, appelé « ramasse-miettes » qui a pour but de surveiller l'utilisation de la mémoire afin de déterminer quand une partie de la mémoire allouée n'est plus utilisée afin de la libérer automatiquement. Ce procédé ne peut être qu'une approximation car savoir si tel ou tel fragment de mémoire est nécessaire est un problème indécidable (autrement dit, ce problème ne peut être résolu par un algorithme).

Le ramasse-miettes ou garbage collection

Comme on vient de le voir, savoir si de la mémoire peut être libérée demeure un problème indécidable. Les ramasses-miettes ne sont donc que des solutions restreintes pour ce problème. La section qui suit détaille les notions importantes pour comprendre ce mécanisme, ainsi que ses limitations.

Références

Le concept principal utilisé par les algorithmes de ramasse-miettes est celui de référence. Dans ce contexte, un objet en référence un autre lorsqu'il a accès à lui (implicitement ou explicitement). Ainsi, un objet JavaScript référencera son prototype (référence implicite) et ses propriétés (référence explicite).

Dans ce contexte, la notion d'objet s'étend et dépasse celle utilisée pour décrire les objets JavaScript, elle contiendra notamment les portées de fonctions (ou la portée globale).

Compter les références

L'algorithme le plus simple consiste à faire l'équivalence entre « un objet n'est plus nécessaire » et « un objet n'a pas d'objet le référençant ». Ainsi un objet peut être « ramassé » par le ramasse-miettes quand il n'y a plus de références pointant vers lui.

Exemple

var o = { 
  a: {
    b:2
  }
}; 
// 2 objets sont créés. L'un est référencé par l'autre en tant que propriété.
// L'autre est référencé car assigné à la variable 'o'.
// Aucun des deux ne peut être ramassé par le ramasse-miettes.


var o2 = o; // la variable 'o2' est le deuxième élément qui
            // référence l'objet o
o = 1;      // désormais, l'objet qui était dans 'o' possède
            // une seule référence de o2 vers lui

var oa = o2.a; // référence la propriété 'a' de l'objet
               // cet objet a donc 2 références : une 
               // par une propriété, l'autre par la variable 'oa'

o2 = "yo"; // L'objet 'o' ne possède plus de références vers lui
           // Il peut être ramassé.
           // Cependant sa propriété 'a' est toujours référencé.
           // La mémoire ne peut donc pas être libérée.

oa = null; // la propriété 'a' ne possède plus de références 
           // vers elle. L'objet peut être ramassé et la mémoire
           // libérée.

Une limitation : les cycles

Cet algorithme est limité car il ne peut pas gérer les cycles (exemple : A référence B et B référence A, ce qui forme un cycle). Avec les cycles, des objets pourraient très bien ne plus être nécessaires et cependant il serait impossible de les ramasser pour libérer la mémoire en utilisant l'algorithme précédent car chaque objet serait référencé au moins une fois et aucun ne pourrait être « ramassé ».

function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o référence o2
  o2.a = o; // o2 référence o

  return "azerty";
}

f();

Exemple réel

Les navigateurs Internet Explorer 6 et 7 utilisent cet algorithme pour gérer les objets du DOM. Certains codes peuvent donc entraîner des fuites de mémoires, en voici un exemple :

var div;
window.onload = function(){
  div = document.getElementById("monÉlémentDiv");
  div.référenceCirculaire = div;
  div.desDonnées = new Array(10000).join("*");
};

Dans cet exemple, l'élément du DOM monÉlémentDiv possède une référence circulaire vers avec lui-même via la propriété référenceCirculaire. Si la propriété n'est pas retirée ou modifiée de façon explicite, un ramasse-miettes qui compte les références aura toujours au moins une référence comptée, ce qui gardera l'élément DOM en mémoire et ce même s'il a été retiré de l'arbre du DOM. Si l'élément du DOM contient beaucoup de données (ce qui est illustré ici avec la propriétés desDonnées), la mémoire consommée par ces données ne sera jamais libérée.

Algorithme « marquer et balayer » (mark-and-sweep)

Cet algorithme réduit la définition « un objet n'est plus nécessaire » à « un objet ne peut être atteint ».

L'utilisation de cet algorithme implique de savoir quels sont les objets racines (en JavaScript, la racine est l'objet global). De façon périodique, le ramasse-miettes commencera par ces racines, listera tous les objets référéncés par ces racines, puis les objets référencés par eux etc. Le ramasse-miettes pourra ainsi construire une liste de tous les objets accessibles et collecter ceux qui ne sont plus accessibles.

Cet algorithme est meilleur que le précédent car la proposition « un objet possède 0 référence » implique « un objet ne peut être atteint ». En revanche, la réciproque n'est pas vraie comme nous avons pu le voir avec les cycles.

En 2012, l'ensemble des navigateurs web modernes disposent d'un ramasse-miettes implémentant cet algorithme mark-and-sweep. L'ensemble des améliorations apportées dans ce domaine de JavaScript représentent des améliorations basées sur cet algorithme, ce ne sont pas de nouveaux algorithmes ou une nouvelle définition pour les objets à supprimer.

Les cycles ne posent plus problème

Dans l'exemple ci-dessus, après le retour de la fonction, les deux objets ne sont plus référencés par quelque chose d'accessible depuis l'objet global. L'algorithme les marquera donc comme « non-accessibles ».

Il en va de même pour le deuxième exemple. Dès lors que le div et la fonction sont rendus inaccessibles depuis les racines, ils peuvent être collectés par le ramasse-miettes malgré le cycle.

Limitation : les objets doivent être effectivement marqués comme inaccessibles

Bien que ceci soit envisagé ici comme une limitation, celle-ci est rarement atteinte en pratique.

Voir aussi

Étiquettes et contributeurs liés au document

Étiquettes : 
Dernière mise à jour par : SphinxKnight,
Masquer la barre latérale