Gestion de la mémoire

Introduction

Les langages de bas niveau, comme C, possèdent des primitives pour gérer la mémoire comme malloc() et free() par exemple. En revanche, quand on utilise JavaScript, la mémoire est allouée à la création des objets puis libérée « automatiquement » lorsqu'ils ne sont plus utilisés. Cette libération automatique est appelée garbage collection ou ramasse-miettes. L'existence de ces procédés est souvent source de confusion et peut donner 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 (lecture, écriture)
  3. Libérer la mémoire allouée lorsqu'on 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 avec 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 variables qu'il contient

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

function f(a){
  return a + 2;
} // alloue de la mémoire pour une fonction (un objet qu'on peut appeler)

// les expressions de fonction alloue aussi de la mémoire
someElement.addEventListener('click', function(){
  someElement.style.backgroundColor = 'blue';
}, false);

Allocation par appels de fonctions

Certains appels de fonctions entraîne une allocation objet.

var d = new Date();
var e = document.createElement('div'); // alloue de la mémoire pour un élément DOM

Certaines méthodes allouent de 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 = ["generation", "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 en cours. Cela peut être effectué lorsqu'on lit ou modifie la valeur d'une variable, propriété ou bien 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 subviennent à 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é à tel endroit du code et la libère.

Les interpréteurs des langages de haut niveau intègrent un composant logiciel, appelé « ramasse-miette » 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 sans qu'on en ait besoin après demure un problème indécidable. Les ramasses-miettes ne sont donc que des solutions restreintes pour ce problèmes. La section qui suit détaillera 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 à celui-là (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.

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éréncé. 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). Pour des cycles, les objets peuvent très bien ne plus être nécessaires et cependant on ne pourrait les ramasser pour libérer la mémoire avec l'algorithme précédent.

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();
// 2 objets sont créés et se référence l'un l'autre (cycle)
// Ils n'appartiennent qu'à la portée de la fonction, inutilisables autre part
// Ils pourraient donc être libérés.
// Cependant, le comptage des références considèrera qu'il y a au moins 1 référence
// ce qui empêchera la libération de la mémoire

Exemple réel

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

var div = document.createElement("div");
div.onclick = function(){
  faireUnTruc();
}; // Le div référence la fonction faireUnTruc grâce à sa propriété onclick' property
// La fonction référence aussi le div car la variable 'div' est dans la portée de la fonction
// Ce cycle entraînera alors une impossibilité de libérer la mémoire d'où une fuite.

Algorithme mark-and-sweep

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

L'utilisation de cet algorithme implique la connaissance d'objets racines (en JavaScript, la racine est l'objet global). À certains moments  le ramasse-miette 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-miette 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 ave les cycles.

En 2012, l'ensemble des navigateurs internet modernes disposent d'un ramasse-miettes implémentant cet algorithme mark-and-sweep. L'esemble des améliorations apportées dans ce domaine de JavaScript représentent des améliorations basées sur cet algorithme.

Les cycles

Dans l'exemple ci-dessus, après le retour de la fonction, les deux objets ne sont 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 dans la pratique.

Voir aussi

Étiquettes et contributeurs liés au document

Étiquettes :
Contributeurs ayant participé à cette page : SphinxKnight
Dernière mise à jour par : SphinxKnight,