Fuite de mémoire - Memory leak

En informatique , une fuite de mémoire est un type de fuite de ressources qui se produit lorsqu'un programme informatique gère de manière incorrecte les allocations de mémoire de manière à ce que la mémoire qui n'est plus nécessaire ne soit pas libérée. Une fuite de mémoire peut également se produire lorsqu'un objet est stocké en mémoire mais n'est pas accessible par le code en cours d'exécution. Une fuite de mémoire présente des symptômes similaires à un certain nombre d'autres problèmes et ne peut généralement être diagnostiquée que par un programmeur ayant accès au code source du programme.

Une fuite d'espace se produit lorsqu'un programme informatique utilise plus de mémoire que nécessaire. Contrairement aux fuites de mémoire, où la mémoire divulguée n'est jamais libérée, la mémoire consommée par une fuite d'espace est libérée, mais plus tard que prévu.

Parce qu'elles peuvent épuiser la mémoire système disponible lors de l'exécution d'une application, les fuites de mémoire sont souvent la cause ou un facteur contribuant au vieillissement des logiciels .

Conséquences

Une fuite de mémoire réduit les performances de l'ordinateur en réduisant la quantité de mémoire disponible. Finalement, dans le pire des cas, une trop grande partie de la mémoire disponible peut être allouée et tout ou partie du système ou de l'appareil cesse de fonctionner correctement, l'application échoue ou le système ralentit considérablement en raison du thrashing .

Les fuites de mémoire peuvent ne pas être graves ou même détectables par des moyens normaux. Dans les systèmes d'exploitation modernes, la mémoire normale utilisée par une application est libérée lorsque l'application se termine. Cela signifie qu'une fuite de mémoire dans un programme qui ne s'exécute que pendant une courte période peut ne pas être remarquée et est rarement grave.

Les fuites beaucoup plus graves incluent celles-ci :

  • où le programme s'exécute pendant une période prolongée et consomme de la mémoire supplémentaire au fil du temps, comme les tâches en arrière-plan sur les serveurs, mais surtout dans les périphériques embarqués qui peuvent fonctionner pendant de nombreuses années
  • où une nouvelle mémoire est allouée fréquemment pour des tâches ponctuelles, comme lors du rendu des images d'un jeu informatique ou d'une vidéo animée
  • où le programme peut demander de la mémoire — comme la mémoire partagée  — qui n'est pas libérée, même lorsque le programme se termine
  • où la mémoire est très limitée, comme dans un système embarqué ou un appareil portable, ou où le programme nécessite une très grande quantité de mémoire pour commencer, laissant peu de marge de fuite
  • où la fuite se produit dans le système d'exploitation ou le gestionnaire de mémoire
  • lorsqu'un pilote de périphérique système provoque la fuite
  • s'exécutant sur un système d'exploitation qui ne libère pas automatiquement de la mémoire à la fin du programme.

Un exemple de fuite de mémoire

L'exemple suivant, écrit en pseudocode , est destiné à montrer comment une fuite de mémoire peut se produire, et ses effets, sans avoir besoin de connaissances en programmation. Le programme dans ce cas fait partie d'un logiciel très simple conçu pour contrôler un ascenseur . Cette partie du programme est exécutée chaque fois que quelqu'un à l'intérieur de l'ascenseur appuie sur le bouton d'un étage.

When a button is pressed:
  Get some memory, which will be used to remember the floor number
  Put the floor number into the memory
  Are we already on the target floor?
    If so, we have nothing to do: finished
    Otherwise:
      Wait until the lift is idle
      Go to the required floor
      Release the memory we used to remember the floor number

La fuite de mémoire se produirait si le numéro d'étage demandé est le même étage que l'ascenseur ; la condition de libération de la mémoire serait ignorée. Chaque fois que ce cas se produit, davantage de mémoire est perdue.

Des cas comme celui-ci n'auraient généralement pas d'effets immédiats. Les gens n'appuient pas souvent sur le bouton de l'étage où ils se trouvent déjà, et dans tous les cas, l'ascenseur peut avoir suffisamment de mémoire disponible pour que cela puisse se produire des centaines ou des milliers de fois. Cependant, l'ascenseur finira par manquer de mémoire. Cela peut prendre des mois ou des années, il se peut donc qu'il ne soit pas découvert malgré des tests approfondis.

Les conséquences seraient désagréables ; à tout le moins, l'ascenseur cesserait de répondre aux demandes de déplacement vers un autre étage (comme lorsqu'une tentative est faite pour appeler l'ascenseur ou lorsque quelqu'un est à l'intérieur et appuie sur les boutons d'étage). Si d'autres parties du programme ont besoin de mémoire (une partie affectée à l'ouverture et à la fermeture de la porte, par exemple), alors personne ne pourra entrer, et si quelqu'un se trouve à l'intérieur, il sera piégé (en supposant que les portes ne puissent pas être ouvert manuellement).

La fuite de mémoire dure jusqu'à ce que le système soit réinitialisé. Par exemple : si l'alimentation de l'ascenseur était coupée ou en cas de panne de courant, le programme s'arrêterait. Lors de la remise sous tension, le programme redémarrerait et toute la mémoire serait à nouveau disponible, mais le lent processus de fuite de mémoire redémarrerait avec le programme, ce qui finirait par nuire au bon fonctionnement du système.

La fuite dans l'exemple ci-dessus peut être corrigée en amenant l'opération « release » en dehors du conditionnel :

When a button is pressed:
  Get some memory, which will be used to remember the floor number
  Put the floor number into the memory
  Are we already on the target floor?
    If not:
      Wait until the lift is idle
      Go to the required floor
  Release the memory we used to remember the floor number

Problèmes de programmation

Les fuites de mémoire sont une erreur courante dans la programmation, en particulier lors de l'utilisation de langages qui n'ont pas de ramasse-miettes automatique intégré , tels que C et C++ . En règle générale, une fuite de mémoire se produit car la mémoire allouée dynamiquement est devenue inaccessible . La prévalence des bugs de fuite de mémoire a conduit au développement d'un certain nombre d' outils de débogage pour détecter la mémoire inaccessible. BoundsChecker , Deleaker , IBM Rational Purify , Valgrind , Parasoft Insure ++ , Dr. Memory et memwatch sont quelques-uns des débogueurs de mémoire les plus populaires pour les programmes C et C++. Des capacités de récupération de place « conservatrices » peuvent être ajoutées à n'importe quel langage de programmation qui en est dépourvu en tant que fonctionnalité intégrée, et des bibliothèques pour ce faire sont disponibles pour les programmes C et C++. Un collectionneur conservateur trouve et récupère la plupart, mais pas la totalité, de la mémoire inaccessible.

Bien que le gestionnaire de mémoire puisse récupérer la mémoire inaccessible, il ne peut pas libérer la mémoire qui est encore accessible et donc potentiellement toujours utile. Les gestionnaires de mémoire modernes fournissent donc aux programmeurs des techniques pour marquer sémantiquement la mémoire avec différents niveaux d'utilité, qui correspondent à différents niveaux d' accessibilité . Le gestionnaire de mémoire ne libère pas un objet fortement accessible. Un objet est fortement atteignable s'il est atteignable soit directement par une référence forte, soit indirectement par une chaîne de références fortes. (Une référence forte est une référence qui, contrairement à une référence faible , empêche un objet d'être récupéré par la mémoire.) Pour éviter cela, le développeur est responsable du nettoyage des références après utilisation, généralement en définissant la référence sur null une fois qu'elle n'est plus nécessaire et, si nécessaire, en désenregistrant tous les écouteurs d'événement qui maintiennent des références fortes à l'objet.

En général, la gestion automatique de la mémoire est plus robuste et pratique pour les développeurs, car ils n'ont pas besoin d'implémenter des routines de libération ou de se soucier de la séquence dans laquelle le nettoyage est effectué ou de se demander si un objet est toujours référencé ou non. Il est plus facile pour un programmeur de savoir quand une référence n'est plus nécessaire que de savoir quand un objet n'est plus référencé. Cependant, la gestion automatique de la mémoire peut imposer une surcharge de performances et n'élimine pas toutes les erreurs de programmation qui provoquent des fuites de mémoire.

RAII

RAII , abréviation de Resource Acquisition Is Initialization , est une approche du problème couramment adoptée en C++ , D et Ada . Cela implique d'associer des objets délimités aux ressources acquises et de libérer automatiquement les ressources une fois que les objets sont hors de portée. Contrairement au ramasse-miettes, RAII a l'avantage de savoir quand les objets existent et quand ils n'existent pas. Comparez les exemples C et C++ suivants :

/* C version */
#include <stdlib.h>

void f(int n)
{
  int* array = calloc(n, sizeof(int));
  do_some_work(array);
  free(array);
}
// C++ version
#include <vector>

void f(int n)
{
  std::vector<int> array (n);
  do_some_work(array);
}

La version C, telle qu'implémentée dans l'exemple, nécessite une désallocation explicite ; le tableau est alloué dynamiquement (à partir du tas dans la plupart des implémentations C) et continue d'exister jusqu'à ce qu'il soit explicitement libéré.

La version C++ ne nécessite aucune désallocation explicite ; il se produira toujours automatiquement dès que l'objet arraysortira de la portée, y compris si une exception est levée. Cela évite une partie de la surcharge des schémas de récupération de place . Et comme les destructeurs d'objets peuvent libérer des ressources autres que la mémoire, RAII aide à empêcher la fuite des ressources d'entrée et de sortie accessibles via un handle , que le ramasse-miettes Mark-and-Sweep ne gère pas correctement. Ceux-ci incluent les fichiers ouverts, les fenêtres ouvertes, les notifications utilisateur, les objets d'une bibliothèque de dessins graphiques, les primitives de synchronisation de threads telles que les sections critiques, les connexions réseau et les connexions au registre Windows ou à une autre base de données.

Cependant, utiliser correctement RAII n'est pas toujours facile et comporte ses propres pièges. Par exemple, si l'on ne fait pas attention, il est possible de créer des pointeurs (ou des références) pendants en renvoyant des données par référence, uniquement pour que ces données soient supprimées lorsque l'objet qui les contient sort de la portée.

D utilise une combinaison de RAII et de récupération de place, employant la destruction automatique lorsqu'il est clair qu'un objet n'est pas accessible en dehors de sa portée d'origine, et la récupération de place dans le cas contraire.

Comptage de références et références cycliques

Les schémas de récupération de place plus modernes sont souvent basés sur une notion d'accessibilité - si vous n'avez pas de référence utilisable à la mémoire en question, elle peut être collectée. D'autres schémas de récupération de place peuvent être basés sur le comptage de références , où un objet est responsable du suivi du nombre de références pointant vers lui. Si le nombre descend à zéro, l'objet est censé se libérer et permettre à sa mémoire d'être récupérée. Le défaut de ce modèle est qu'il ne gère pas les références cycliques, et c'est pourquoi de nos jours, la plupart des programmeurs sont prêts à accepter le fardeau des systèmes de marquage et de balayage plus coûteux .

Le code Visual Basic suivant illustre la fuite de mémoire de comptage de références canonique :

Dim A, B
Set A = CreateObject("Some.Thing")
Set B = CreateObject("Some.Thing")
' At this point, the two objects each have one reference,

Set A.member = B
Set B.member = A
' Now they each have two references.

Set A = Nothing   ' You could still get out of it...

Set B = Nothing   ' And now you've got a memory leak!

End

En pratique, cet exemple trivial serait tout de suite repéré et corrigé. Dans la plupart des exemples réels, le cycle de références s'étend sur plus de deux objets et est plus difficile à détecter.

Un exemple bien connu de ce type de fuite a pris de l'importance avec la montée en puissance des techniques de programmation AJAX dans les navigateurs Web dans le problème des auditeurs inactifs . Le code JavaScript qui associait un élément DOM à un gestionnaire d'événements et ne parvenait pas à supprimer la référence avant de quitter, entraînerait une fuite de mémoire (les pages Web AJAX conservent un DOM donné beaucoup plus longtemps que les pages Web traditionnelles, donc cette fuite était beaucoup plus apparente) .

Effets

Si un programme a une fuite de mémoire et que son utilisation de la mémoire augmente régulièrement, il n'y aura généralement pas de symptôme immédiat. Chaque système physique a une quantité limitée de mémoire, et si la fuite de mémoire n'est pas contenue (par exemple, en redémarrant le programme de fuite), cela finira par causer des problèmes.

La plupart des systèmes d'exploitation de bureau grand public modernes ont à la fois une mémoire principale qui est physiquement logée dans des puces RAM et un stockage secondaire tel qu'un disque dur . L'allocation de mémoire est dynamique – chaque processus obtient autant de mémoire qu'il en demande. Les pages actives sont transférées dans la mémoire principale pour un accès rapide ; les pages inactives sont transférées vers le stockage secondaire pour faire de la place, selon les besoins. Lorsqu'un seul processus commence à consommer une grande quantité de mémoire, il occupe généralement de plus en plus de mémoire principale, poussant les autres programmes vers le stockage secondaire, ce qui ralentit généralement considérablement les performances du système. Même si le programme qui fuit est terminé, il peut s'écouler un certain temps avant que d'autres programmes reviennent dans la mémoire principale et que les performances reviennent à la normale.

Lorsque toute la mémoire d'un système est épuisée (qu'il y ait de la mémoire virtuelle ou uniquement de la mémoire principale, comme sur un système embarqué), toute tentative d'allouer plus de mémoire échouera. Cela provoque généralement l'arrêt du programme qui tente d'allouer la mémoire ou la génération d'une erreur de segmentation . Certains programmes sont conçus pour se remettre de cette situation (éventuellement en se rabattant sur la mémoire pré-réservée). Le premier programme à rencontrer le manque de mémoire peut ou non être le programme qui a la fuite de mémoire.

Certains systèmes d'exploitation multitâches ont des mécanismes spéciaux pour faire face à une condition de mémoire insuffisante, comme l'élimination aléatoire de processus (ce qui peut affecter des processus « innocents »), ou l'élimination du plus gros processus en mémoire (qui est vraisemblablement celui qui cause le problème). Certains systèmes d'exploitation ont une limite de mémoire par processus, pour éviter qu'un programme ne monopolise toute la mémoire du système. L'inconvénient de cette disposition est que le système d'exploitation doit parfois être reconfiguré pour permettre le bon fonctionnement des programmes qui nécessitent légitimement de grandes quantités de mémoire, tels que ceux traitant des graphiques, de la vidéo ou des calculs scientifiques.

Le modèle « en dents de scie » d'utilisation de la mémoire : la baisse soudaine de la mémoire utilisée est un symptôme candidat à une fuite de mémoire.

Si la fuite de mémoire est dans le noyau , le système d'exploitation lui-même échouera probablement. Les ordinateurs sans gestion de mémoire sophistiquée, tels que les systèmes embarqués, peuvent également échouer complètement à cause d'une fuite de mémoire persistante.

Les systèmes accessibles au public tels que les serveurs Web ou les routeurs sont sujets aux attaques par déni de service si un attaquant découvre une séquence d'opérations pouvant déclencher une fuite. Une telle séquence est connue sous le nom d' exploit .

Un modèle "en dents de scie" d'utilisation de la mémoire peut être un indicateur d'une fuite de mémoire au sein d'une application, en particulier si les chutes verticales coïncident avec des redémarrages ou des redémarrages de cette application. Des précautions doivent cependant être prises car les points de collecte des ordures peuvent également provoquer un tel modèle et indiquer une utilisation saine du tas.

Autres consommateurs de mémoire

Notez que l'augmentation constante de l'utilisation de la mémoire n'est pas nécessairement la preuve d'une fuite de mémoire. Certaines applications stockeront des quantités toujours croissantes d'informations en mémoire (par exemple sous forme de cache ). Si le cache peut devenir si volumineux qu'il peut causer des problèmes, il peut s'agir d'une erreur de programmation ou de conception, mais il ne s'agit pas d'une fuite de mémoire car les informations restent nominalement utilisées. Dans d'autres cas, les programmes peuvent nécessiter une quantité de mémoire déraisonnable car le programmeur a supposé que la mémoire est toujours suffisante pour une tâche particulière ; par exemple, un processeur de fichiers graphiques peut commencer par lire l'intégralité du contenu d'un fichier image et le stocker en mémoire, ce qui n'est pas viable lorsqu'une très grande image dépasse la mémoire disponible.

En d'autres termes, une fuite de mémoire résulte d'un type particulier d'erreur de programmation, et sans accès au code du programme, une personne voyant des symptômes ne peut que deviner qu'il pourrait y avoir une fuite de mémoire. Il serait préférable d'utiliser des termes tels que "utilisation de la mémoire en constante augmentation" lorsqu'aucune connaissance interne de ce type n'existe.

Un exemple simple en C

La fonction C suivante fuit délibérément de la mémoire en perdant le pointeur vers la mémoire allouée. On peut dire que la fuite se produit dès que le pointeur 'a' sort de la portée, c'est-à-dire lorsque function_which_allocates() retourne sans libérer 'a'.

#include <stdlib.h>

void function_which_allocates(void) {
    /* allocate an array of 45 floats */
    float *a = malloc(sizeof(float) * 45);

    /* additional code making use of 'a' */

    /* return to main, having forgotten to free the memory we malloc'd */
}

int main(void) {
    function_which_allocates();

    /* the pointer 'a' no longer exists, and therefore cannot be freed,
     but the memory is still allocated. a leak has occurred. */
}

Voir également

Les références

Liens externes