Looking for python Keywords? Try Ask4Keywords

Python LanguageCollecte des ordures


Remarques

À la base, le ramasse-miettes de Python (à partir de 3.5) est une simple implémentation de comptage de références. Chaque fois que vous faites une référence à un objet (par exemple, a = myobject ), le nombre de références sur cet objet (myobject) est incrémenté. Chaque fois qu'une référence est supprimée, le compte de référence est décrémenté, et une fois que le compte de référence atteint 0 , nous savons que rien ne contient de référence à cet objet et que nous pouvons le désallouer!

Un des malentendus courants concernant le fonctionnement de la gestion de la mémoire Python est que le mot-clé del libère la mémoire des objets. Ce n'est pas vrai. En fait, le mot-clé del ne fait que décrémenter les objets refcount, ce qui signifie que si vous l'appelez suffisamment de fois pour que le refcount atteigne zéro, l'objet peut être récupéré (même s'il existe encore des références à l'objet disponible ailleurs dans votre code) ).

Python crée ou nettoie agressivement des objets la première fois qu'il en a besoin Si j'effectue l'affectation a = object (), la mémoire de l'objet est allouée à ce moment (cpython réutilisera parfois certains types d'objet, par exemple des listes sous le capot). mais la plupart du temps, il ne conserve pas de pool d’objets gratuits et effectue des allocations quand vous en avez besoin. De même, dès que le refcount est décrémenté à 0, GC le nettoie.

Collecte de déchets générationnelle

Dans les années 1960, John McCarthy a découvert une erreur fatale dans le refcounting de garbage collection lorsqu'il implémentait l'algorithme de refcounting utilisé par Lisp: que se passe-t-il si deux objets se réfèrent l'un à l'autre dans une référence cyclique? Comment pouvez-vous jamais ramasser ces deux objets même s'il n'y a pas de références externes à ces objets s'ils se réfèrent toujours l'un à l'autre? Ce problème s'étend également à toute structure de données cyclique, telle que les tampons en anneau ou deux entrées consécutives dans une liste à double liaison. Python tente de résoudre ce problème en utilisant une variante légèrement intéressante d'un autre algorithme de récupération de place appelé Generational Garbage Collection .

Essentiellement, chaque fois que vous créez un objet en Python, il l'ajoute à la fin d'une liste doublement liée. À l'occasion, Python fait une boucle dans cette liste, vérifie quels objets font référence aux objets de la liste et, s'ils se trouvent également dans la liste (nous verrons pourquoi ils ne le seront pas dans un instant), réduit davantage leurs refcounts. À ce stade (en fait, certaines heuristiques déterminent le moment où les choses sont déplacées, mais supposons que, après une seule collection, les choses restent simples), tout ce qui a un refcount supérieur à 0 est promu dans une autre liste appelée "Génération 1" (C'est pourquoi tous les objets ne figurent pas toujours dans la liste de génération 0), qui a cette boucle appliquée moins souvent. C'est ici qu'intervient la récupération de mémoire générationnelle. Il existe 3 générations par défaut dans Python (trois listes d'objets liées): La première liste (génération 0) contient tous les nouveaux objets; Si un cycle de CPG se produit et que les objets ne sont pas collectés, ils sont déplacés vers la deuxième liste (génération 1) et si un cycle de CP survient sur la deuxième liste et qu'ils ne sont toujours pas collectés, ils sont déplacés vers la troisième liste (génération 2) ). La liste de troisième génération (appelée "génération 2", puisque nous avons une indexation nulle) est beaucoup moins souvent collectée que les deux premières, l’idée étant que si votre objet a une longue vie, il est peu probable être GCed pendant la durée de vie de votre application, il est donc inutile de perdre du temps à la vérifier sur chaque cycle de GC. De plus, on observe que la plupart des objets sont collectés relativement rapidement. A partir de maintenant, nous appellerons ces "bons objets" car ils meurent jeunes. Cela s'appelle "l'hypothèse générationnelle faible" et a également été observé pour la première fois dans les années soixante.

Une mise de côté rapide: contrairement aux deux premières générations, la liste de troisième génération de longue durée de vie n’est pas une collecte régulière des ordures. Il est vérifié lorsque le rapport entre les objets en attente de longue durée (ceux qui figurent dans la liste de troisième génération, mais qui n'ont pas encore eu de cycle GC) avec le nombre total d'objets à vie longue de la liste est supérieur à 25%. C'est parce que la troisième liste est illimitée (les choses ne sont jamais déplacées d'une autre liste, donc elles ne disparaissent que lorsqu'elles sont réellement récupérées), ce qui signifie que pour les applications où vous créez beaucoup d'objets à vie longue sur la troisième liste peut être assez long. En utilisant un ratio, nous obtenons une "performance linéaire amortie dans le nombre total d'objets"; alias, plus la liste est longue, plus le GC prend de temps, mais moins nous effectuons de GC (voici la proposition originale de 2008 pour cette heuristique de Martin von Löwis). Le fait d'effectuer une récupération de place sur la troisième génération ou la liste "mature" est appelée "récupération complète".

La récupération de place générationnelle accélère donc les choses en n'exigeant pas que nous analysions des objets qui n'auront probablement pas besoin de GC tout le temps, mais comment cela nous aide-t-il à casser des références cycliques? Probablement pas très bien, il se trouve que La fonction de rupture de ces cycles de référence commence ainsi :

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)

La raison pour laquelle la récupération de la mémoire générationnelle aide à cela est que nous pouvons conserver la longueur de la liste en tant que compte distinct; à chaque fois que nous ajoutons un nouvel objet à la génération, nous incrémentons ce nombre, et chaque fois que nous déplaçons un objet vers une autre génération ou un dealloc, nous décrémentons le nombre. Théoriquement, à la fin d'un cycle de CPG, ce compte (pour les deux premières générations de toute façon) devrait toujours être 0. Si ce n'est pas le cas, tout élément de la liste est une forme de référence circulaire. Cependant, il y a un autre problème ici: que se passe-t-il si les objets restants ont la méthode magique de Python __del__ sur eux? __del__ est appelé à chaque fois qu'un objet Python est détruit. Cependant, si deux objets dans une référence circulaire ont des méthodes __del__ , nous ne pouvons pas être sûrs que leur destruction ne va pas casser la méthode __del__ autres. Pour un exemple artificiel, imaginons que nous avons écrit ce qui suit:

class A(object):
    def __init__(self, b=None):
        self.b = b
 
    def __del__(self):
        print("We're deleting an instance of A containing:", self.b)
     
class B(object):
    def __init__(self, a=None):
        self.a = a
 
    def __del__(self):
        print("We're deleting an instance of B containing:", self.a)

et nous définissons une instance de A et une instance de B pour qu'elles se pointent l'une sur l'autre et qu'elles se retrouvent dans le même cycle de récupération de place? Disons que nous en sélectionnons un au hasard et distribuons notre instance de A en premier; La méthode __del__ de A sera appelée, elle sera imprimée, puis A sera libérée. Ensuite, nous arrivons à B, nous appelons sa méthode __del__ , et oops! Segfault! A n'existe plus. Nous pourrions résoudre ce problème en appelant tout ce qui reste des méthodes __del__ , puis en effectuant une autre passe pour tout désassembler. Cependant, ceci introduit un autre problème: si une méthode __del__ enregistre une référence de l’autre objet a une référence à nous ailleurs? Nous avons toujours un cycle de référence, mais maintenant il n'est plus possible de GC non plus l'objet, même s'ils ne sont plus utilisés. Notez que même si un objet ne fait pas partie d'une structure de données circulaire, il pourrait se __del__ dans sa propre méthode __del__ ; Python vérifie cela et arrêtera GCing si un refcount d'objets a augmenté après l' __del__ sa méthode __del__ .

CPython s'occupe de cela en collant ces objets non-GC-possibles (n'importe quoi avec une forme de référence circulaire et une méthode __del__ ) sur une liste globale de déchets irrécupérables et en les laissant pour toute l'éternité:

/* list of uncollectable objects */
static PyObject *garbage = NULL;

Collecte des ordures Exemples Liés