Python Language
Collecte des ordures
Recherche…
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;
Comptage de référence
La grande majorité de la gestion de la mémoire Python est gérée avec le comptage des références.
Chaque fois qu'un objet est référencé (par exemple, affecté à une variable), son compte de référence est automatiquement augmenté. Quand il est déréférencé (par exemple, la variable sort de la portée), son compte de référence est automatiquement diminué.
Lorsque le compteur de références atteint zéro, l'objet est immédiatement détruit et la mémoire est immédiatement libérée. Ainsi, dans la majorité des cas, le ramasse-miettes n'est même pas nécessaire.
>>> import gc; gc.disable() # disable garbage collector
>>> class Track:
def __init__(self):
print("Initialized")
def __del__(self):
print("Destructed")
>>> def foo():
Track()
# destructed immediately since no longer has any references
print("---")
t = Track()
# variable is referenced, so it's not destructed yet
print("---")
# variable is destructed when function exits
>>> foo()
Initialized
Destructed
---
Initialized
---
Destructed
Démontrer plus avant le concept de références:
>>> def bar():
return Track()
>>> t = bar()
Initialized
>>> another_t = t # assign another reference
>>> print("...")
...
>>> t = None # not destructed yet - another_t still refers to it
>>> another_t = None # final reference gone, object is destructed
Destructed
Garbage Collector pour les cycles de référence
La seule fois où le ramasse-miettes est nécessaire, c'est si vous avez un cycle de référence . L'exemple simple d'un cycle de référence est celui dans lequel A désigne B et B désigne A, tandis que rien d'autre ne fait référence à A ou B. Ni A ni B ne sont accessibles de n'importe où dans le programme, ils peuvent donc être détruits en toute sécurité. cependant, leurs comptages de référence sont 1 et ils ne peuvent donc pas être libérés par l'algorithme de comptage de référence seul.
>>> import gc; gc.disable() # disable garbage collector
>>> class Track:
def __init__(self):
print("Initialized")
def __del__(self):
print("Destructed")
>>> A = Track()
Initialized
>>> B = Track()
Initialized
>>> A.other = B
>>> B.other = A
>>> del A; del B # objects are not destructed due to reference cycle
>>> gc.collect() # trigger collection
Destructed
Destructed
4
Un cycle de référence peut être arbitrairement long. Si A pointe vers B pointe vers C pointe sur ... pointe vers Z qui pointe sur A, alors ni A ni Z ne seront collectés, jusqu'à la phase de récupération de place:
>>> objs = [Track() for _ in range(10)]
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
Initialized
>>> for i in range(len(objs)-1):
... objs[i].other = objs[i + 1]
...
>>> objs[-1].other = objs[0] # complete the cycle
>>> del objs # no one can refer to objs now - still not destructed
>>> gc.collect()
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
Destructed
20
Effets de la commande del
Suppression d'un nom de variable de l'étendue à l'aide de del v
ou suppression d'un objet d'une collection à l'aide de del v[item]
ou del[i:j]
ou suppression d'un attribut avec del v.name
ou tout autre moyen de suppression de références un objet ne déclenche aucun appel de destructeur ni aucune mémoire libérée en soi. Les objets ne sont détruits que lorsque leur compte de référence atteint zéro.
>>> import gc
>>> gc.disable() # disable garbage collector
>>> class Track:
def __init__(self):
print("Initialized")
def __del__(self):
print("Destructed")
>>> def bar():
return Track()
>>> t = bar()
Initialized
>>> another_t = t # assign another reference
>>> print("...")
...
>>> del t # not destructed yet - another_t still refers to it
>>> del another_t # final reference gone, object is destructed
Destructed
Réutilisation d'objets primitifs
Une chose intéressante à noter qui peut aider à optimiser vos applications est que les primitives sont en fait également recalculées sous le capot. Jetons un coup d'oeil aux chiffres; pour tous les entiers compris entre -5 et 256, Python réutilise toujours le même objet:
>>> import sys
>>> sys.getrefcount(1)
797
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
799
Notez que le refcount augmente, ce qui signifie que a
et b
référencent le même objet sous-jacent lorsqu'ils font référence à la primitive 1
. Cependant, pour les nombres plus importants, Python ne réutilise pas réellement l'objet sous-jacent:
>>> a = 999999999
>>> sys.getrefcount(999999999)
3
>>> b = 999999999
>>> sys.getrefcount(999999999)
3
Étant donné que le refcount pour 999999999
ne change pas lors de l'affectation à a
et b
nous pouvons en déduire qu'ils font référence à deux objets sous-jacents différents, même s'ils se voient attribuer la même primitive.
Affichage du refcount d'un objet
>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2
Désallouer des objets avec force
Vous pouvez forcer les objets de désaffectation même si leur refcount n'est pas 0 dans Python 2 et 3.
Les deux versions utilisent le module ctypes
pour le faire.
AVERTISSEMENT: cela laissera faire votre environnement Python instable et enclin à s'écraser sans retraçage! L'utilisation de cette méthode peut également créer des problèmes de sécurité (très improbable). Déjà.
import ctypes
deallocated = 12345
ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
import ctypes, sys
deallocated = 12345
(ctypes.c_char * sys.getsizeof(deallocated)).from_address(id(deallocated))[:4] = '\x00' * 4
Après l'exécution, toute référence à l'objet maintenant désalloué entraînera un comportement ou un blocage indéfini de Python, sans trace. Il y avait probablement une raison pour laquelle le garbage collector n'a pas supprimé cet objet ...
Si vous désallouez None
, vous obtenez un message spécial - Fatal Python error: deallocating None
avant de planter.
Gestion de la récupération de place
Il existe deux approches pour influencer le nettoyage de la mémoire. Ils influencent la fréquence à laquelle le processus automatique est exécuté et l'autre déclenche manuellement un nettoyage.
Le garbage collector peut être manipulé en réglant les seuils de collecte qui affectent la fréquence d'exécution du collecteur. Python utilise un système de gestion de la mémoire basé sur la génération. Les nouveaux objets sont enregistrés dans la génération la plus récente - generation0 et avec chaque collection survivante, les objets sont promus aux générations plus anciennes. Après avoir atteint la dernière génération - la génération 2 , ils ne sont plus promus.
Les seuils peuvent être modifiés à l'aide de l'extrait suivant:
import gc
gc.set_threshold(1000, 100, 10) # Values are just for demonstration purpose
Le premier argument représente le seuil de collecte de génération0 . Chaque fois que le nombre d' allocations dépasse le nombre de désallocations de 1 000, le récupérateur de mémoire sera appelé.
Les générations les plus anciennes ne sont pas nettoyées à chaque exécution pour optimiser le processus. Les deuxième et troisième arguments sont facultatifs et contrôlent la fréquence de nettoyage des générations les plus anciennes. Si generation0 a été traité 100 fois sans nettoyage de la génération 1 , alors la génération 1 sera traitée. De même, les objets de génération 2 ne seront traités que lorsque ceux de génération 1 ont été nettoyés 10 fois sans toucher à la génération 2 .
Un exemple dans lequel la définition manuelle des seuils est bénéfique est lorsque le programme alloue un grand nombre de petits objets sans les désallouer, ce qui entraîne l'exécution trop fréquente du ramasse-miettes (chaque attribution d'objet generation0_threshold ). Bien que le collecteur soit assez rapide, lorsqu'il fonctionne sur un grand nombre d'objets, il pose un problème de performance. Quoi qu'il en soit, il n'y a pas de stratégie unique pour choisir les seuils et son utilisation est fiable.
Le déclenchement manuel d'une collection peut se faire comme dans l'extrait de code suivant:
import gc
gc.collect()
La récupération de place est automatiquement déclenchée en fonction du nombre d'allocations et de désallocations, et non de la mémoire consommée ou disponible. Par conséquent, lorsque vous travaillez avec des objets volumineux, la mémoire peut être épuisée avant le déclenchement du nettoyage automatisé. Cela fait un bon cas d'utilisation pour appeler manuellement le garbage collector.
Même si c'est possible, ce n'est pas une pratique encouragée. Eviter les fuites de mémoire est la meilleure option. Quoi qu'il en soit, dans les grands projets, la détection de la fuite de mémoire peut être une tâche ardue et le déclenchement manuel d'une récupération de place peut être utilisé comme solution rapide jusqu'au débogage ultérieur.
Pour les programmes de longue durée, le nettoyage de la mémoire peut être déclenché sur une base ponctuelle ou par événement. Un exemple pour le premier est un serveur Web qui déclenche une collecte après un nombre fixe de requêtes. Par la suite, un serveur Web qui déclenche un nettoyage de la mémoire lorsqu'un certain type de demande est reçu.
N'attendez pas que le ramasse-miettes nettoie
Le fait que le ramasse-miettes se nettoie ne signifie pas que vous devez attendre le nettoyage du cycle de nettoyage.
En particulier, vous ne devez pas attendre le nettoyage de la mémoire pour fermer les descripteurs de fichiers, les connexions à la base de données et les connexions réseau ouvertes.
par exemple:
Dans le code suivant, vous supposez que le fichier sera fermé lors du prochain cycle de récupération de la mémoire, si f était la dernière référence au fichier.
>>> f = open("test.txt")
>>> del f
Un moyen plus explicite de nettoyer est d'appeler f.close()
. Vous pouvez le faire encore plus élégant, en utilisant l'instruction with
, également appelée gestionnaire de contexte :
>>> with open("test.txt") as f:
... pass
... # do something with f
>>> #now the f object still exists, but it is closed
L'instruction with
vous permet d'indenter votre code sous le fichier ouvert. Cela rend explicite et plus facile de voir combien de temps un fichier reste ouvert. Il ferme également toujours un fichier, même si une exception est déclenchée dans le bloc while
.