Zoeken…


Opmerkingen

In de kern is de afvalverzamelaar van Python (vanaf 3.5) een eenvoudige implementatie voor het tellen van naslagwerken. Telkens wanneer u naar een object verwijst (bijvoorbeeld a = myobject ), wordt de referentietelling voor dat object (myobject) verhoogd. Telkens wanneer een referentie wordt verwijderd, wordt de referentietelling verlaagd en zodra de referentietelling 0 bereikt, weten we dat niets een verwijzing naar dat object bevat en kunnen we de toewijzing ongedaan maken!

Een veel voorkomend misverstand over hoe Python-geheugenbeheer werkt, is dat het del trefwoord objectengeheugen vrijmaakt. Dit is niet waar. Wat er feitelijk gebeurt, is dat het del sleutelwoord alleen de refcount van de objecten verlaagt, wat betekent dat als je het genoeg keer noemt om de refcount nul te laten worden, het object afval kan worden verzameld (zelfs als er nog steeds verwijzingen naar het object elders in je code beschikbaar zijn ).

Python maakt of maakt objecten agressief de eerste keer dat het ze nodig heeft. Als ik de opdracht a = object () uitvoer, wordt het geheugen voor object op dat moment toegewezen (cpython zal soms bepaalde soorten objecten hergebruiken, bijv. Lijsten onder de motorkap, maar meestal houdt het geen gratis objectpool bij en voert het een toewijzing uit wanneer u het nodig hebt). Op dezelfde manier ruimt GC het op zodra de refcount is verlaagd naar 0.

Generational Garbage Collection

In de jaren 1960 ontdekte John McCarthy een fatale fout in de herverzameling van afvalinzameling toen hij het herverrekeningsalgoritme implementeerde dat door Lisp werd gebruikt: wat gebeurt er als twee objecten naar elkaar verwijzen in een cyclische referentie? Hoe kun je die twee objecten ooit verzamelen, zelfs als er geen externe verwijzingen naar zijn, als ze altijd naar elkaar verwijzen? Dit probleem is ook van toepassing op elke cyclische gegevensstructuur, zoals ringbuffers of twee opeenvolgende vermeldingen in een dubbel gekoppelde lijst. Python probeert dit probleem op te lossen met een ietwat interessante draai aan een ander algoritme voor het verzamelen van afval, genaamd Generational Garbage Collection .

In essentie wordt het telkens wanneer u een object in Python maakt, aan het einde van een dubbel gekoppelde lijst toegevoegd. Af en toe doorzoekt Python deze lijst en controleert het naar de objecten waarnaar de objecten in de lijst verwijzen, en als ze ook in de lijst staan (we zullen zien waarom ze misschien niet zo zijn), vermindert hun refcounts verder. Op dit punt (eigenlijk zijn er enkele heuristieken die bepalen wanneer dingen worden verplaatst, maar laten we aannemen dat het na een enkele verzameling is om dingen eenvoudig te houden) alles met een refcount groter dan 0 wordt gepromoveerd naar een andere gekoppelde lijst met de naam "Generatie 1" (dit is waarom alle objecten niet altijd in de generatie 0-lijst staan) waarop deze lus minder vaak wordt toegepast. Hier komt de generational garbage collection. Er zijn standaard 3 generaties in Python (drie gekoppelde lijsten met objecten): De eerste lijst (generatie 0) bevat alle nieuwe objecten; als een GC-cyclus plaatsvindt en de objecten niet worden verzameld, worden ze verplaatst naar de tweede lijst (generatie 1), en als een GC-cyclus op de tweede lijst gebeurt en ze nog steeds niet worden verzameld, worden ze verplaatst naar de derde lijst (generatie 2) ). De derde generatielijst ("generatie 2" genoemd, omdat we nulindexeren) is afval dat veel minder vaak wordt verzameld dan de eerste twee, het idee is dat als je object lang meegaat, het niet zo waarschijnlijk is dat het GCed is en misschien nooit wees GCed tijdens de levensduur van uw applicatie, dus het heeft geen zin om tijd te verspillen door het te controleren bij elke GC-run. Verder wordt opgemerkt dat de meeste objecten relatief snel afval worden verzameld. Vanaf nu noemen we deze 'goede objecten' omdat ze jong sterven. Dit wordt de 'zwakke generatiehypothese' genoemd en werd ook voor het eerst waargenomen in de jaren '60.

Even terzijde: in tegenstelling tot de eerste twee generaties, is de langlevende lijst van de derde generatie geen afval dat regelmatig wordt ingezameld. Het wordt gecontroleerd wanneer de verhouding tussen langlevende objecten in afwachting (die in de derde generatielijst staan, maar nog geen GC-cyclus hebben gehad) en het totale aantal langlevende objecten in de lijst groter is dan 25%. Dit komt omdat de derde lijst onbegrensd is (dingen worden er nooit van verplaatst naar een andere lijst, dus ze gaan alleen weg als ze daadwerkelijk afval zijn verzameld), wat betekent dat voor toepassingen waar u veel langlevende objecten maakt, GC-cycli op de derde lijst kan behoorlijk lang worden. Door een verhouding te gebruiken, bereiken we "geamortiseerde lineaire prestaties in het totale aantal objecten"; aka, hoe langer de lijst, hoe langer GC duurt, maar hoe minder vaak we GC uitvoeren (hier is het oorspronkelijke 2008-voorstel voor deze heuristiek door Martin von Löwis voor verder lezen). Het uitvoeren van een afvalinzameling op de derde generatie of "volwassen" lijst wordt "volledige afvalinzameling" genoemd.

Dus de generational garbage collection versnelt de dingen enorm door niet te vereisen dat we over objecten scannen die waarschijnlijk niet altijd GC nodig hebben, maar hoe helpt het ons cyclische referenties te doorbreken? Waarschijnlijk niet zo goed, zo blijkt. De functie voor het daadwerkelijk verbreken van deze referentiecycli begint als volgt :

/* 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)

De reden waarom generational garbage collection hierbij helpt, is dat we de lengte van de lijst als een afzonderlijke telling kunnen behouden; elke keer dat we een nieuw object aan de generatie toevoegen, verhogen we deze telling, en elke keer dat we een object naar een andere generatie verplaatsen of dealloceren, verlagen we de telling. Theoretisch gezien zou aan het einde van een GC-cyclus deze telling (in elk geval voor de eerste twee generaties) altijd 0 moeten zijn. Er is echter nog een probleem: wat als de overgebleven objecten de magische methode __del__ van Python hebben? __del__ wordt altijd genoemd wanneer een Python-object wordt vernietigd. Als twee objecten in een cirkelvormige verwijzing echter __del__ methoden hebben, kunnen we er niet zeker van zijn dat het vernietigen van het ene de andere __del__ methode niet zal breken. Stel je voor een gekunsteld voorbeeld dat we het volgende schreven:

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)

en we stellen een instantie van A en een instantie van B in om naar elkaar te wijzen en dan belanden ze in dezelfde afvalinzamelcyclus? Laten we zeggen dat we er willekeurig een kiezen en onze instantie van A eerst dealloceren; De __del__ methode van A wordt aangeroepen, deze wordt afgedrukt en vervolgens wordt A vrijgegeven. Vervolgens komen we bij B, we noemen de __del__ methode en oeps! Segfault! A bestaat niet meer. We kunnen dit oplossen door eerst alles te roepen wat overblijft in de __del__ methoden, en vervolgens een andere pass te doen om eigenlijk alles te dealloceren, maar dit introduceert een ander probleem: wat als de ene object __del__ methode een referentie opslaat van het andere object dat GCed gaat worden en heeft ergens anders een verwijzing naar ons? We hebben nog steeds een referentiecyclus, maar nu is het niet mogelijk om beide objecten daadwerkelijk te GC, zelfs als ze niet langer in gebruik zijn. Merk op dat zelfs als een object geen deel uitmaakt van een circulaire datastructuur, het zichzelf kan herleven in zijn eigen __del__ methode; Python heeft hier een vinkje voor en zal GCing stoppen als de refcount van een object is toegenomen nadat de __del__ methode is aangeroepen.

CPython behandelt dit door deze niet-GC-compatibele objecten (alles met een vorm van circulaire referentie en een __del__ methode) op een wereldwijde lijst van oninbaar afval te __del__ en het daar voor eeuwig te laten:

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

Referentietelling

Het overgrote deel van Python-geheugenbeheer wordt afgehandeld met referentietellingen.

Telkens wanneer naar een object wordt verwezen (bijvoorbeeld toegewezen aan een variabele), wordt het referentietelling automatisch verhoogd. Wanneer er naar een verwijzing wordt verwezen (variabele valt bijvoorbeeld buiten het bereik), wordt het referentietelling automatisch verlaagd.

Wanneer de referentietelling nul bereikt, wordt het object onmiddellijk vernietigd en wordt het geheugen onmiddellijk vrijgegeven. In de meeste gevallen is de vuilnisman dus niet eens nodig.

>>> 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

Om het concept van referenties verder te demonstreren:

>>> 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 voor referentiecycli

De enige keer dat de vuilnisman nodig is, is als u een referentiecyclus heeft . Het eenvoudige voorbeeld van een referentiecyclus is er een waarin A verwijst naar B en B verwijst naar A, terwijl niets anders verwijst naar A of B. Noch A of B zijn overal toegankelijk in het programma, zodat ze veilig kunnen worden vernietigd, maar hun referentietellingen zijn 1 en dus kunnen ze niet worden bevrijd door het referentietellingsalgoritme alleen.

>>> 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

Een referentiecyclus kan willekeurig lang zijn. Als A naar B wijst naar C naar ... wijst naar Z die naar A wijst, dan worden geen van beide A tot en met Z verzameld tot de fase van de afvalinzameling:

>>> 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

Effecten van de opdracht del

Een variabelenaam verwijderen uit het bereik met del v , of een object verwijderen uit een verzameling met del v[item] of del[i:j] , of een attribuut verwijderen met del v.name , of een andere manier om verwijzingen naar te verwijderen een object, geen destructor oproepen activeren of geheugen bevrijd op zich. Objecten worden alleen vernietigd wanneer hun referentietelling nul bereikt.

>>> 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

Hergebruik van primitieve objecten

Een interessant ding om op te merken dat kan helpen uw toepassingen te optimaliseren, is dat primitieven ook opnieuw worden getild onder de motorkap. Laten we eens naar getallen kijken; voor alle gehele getallen tussen -5 en 256, gebruikt Python altijd hetzelfde object:

>>> import sys
>>> sys.getrefcount(1)
797
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
799

Merk op dat de refcount toeneemt, wat betekent dat a en b hetzelfde onderliggende object verwijzen als ze naar de 1 primitieve verwijzen. Voor grotere aantallen hergebruikt Python het onderliggende object echter niet:

>>> a = 999999999
>>> sys.getrefcount(999999999)
3
>>> b = 999999999
>>> sys.getrefcount(999999999)
3

Omdat de refcount voor 999999999 niet verandert wanneer toe te wijzen aan a en b kunnen we afleiden dat ze verwijzen naar twee verschillende onderliggende objecten, hoewel ze allebei hetzelfde primitieve toegewezen.

De refcount van een object bekijken

>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2

Objecten krachtig dealloceren

Je kunt deallocatie van objecten forceren, zelfs als hun refcount niet 0 is in zowel Python 2 als 3.

Beide versies gebruiken hiervoor de module ctypes .

WAARSCHUWING: als u dit doet , blijft uw Python-omgeving onstabiel en kan het crashen zonder traceback! Het gebruik van deze methode kan ook beveiligingsproblemen veroorzaken (vrij onwaarschijnlijk). Alleen objecten dealloceren waarvan u zeker weet dat u er nooit meer naar zult verwijzen. Ooit.

Python 3.x 3.0
import ctypes
deallocated = 12345
ctypes.pythonapi._Py_Dealloc(ctypes.py_object(deallocated))
Python 2.x 2.3
import ctypes, sys
deallocated = 12345
(ctypes.c_char * sys.getsizeof(deallocated)).from_address(id(deallocated))[:4] = '\x00' * 4

Na het uitvoeren zal elke verwijzing naar het nu niet-toegewezen object ervoor zorgen dat Python ofwel ongedefinieerd gedrag produceert of crasht - zonder een traceback. Er was waarschijnlijk een reden waarom de vuilnisman dat object niet had verwijderd ...

Als je None dealloceert, krijg je een speciaal bericht - Fatal Python error: deallocating None voordat je crasht.

Afvalinzameling beheren

Er zijn twee benaderingen om te beïnvloeden wanneer een geheugenopruiming wordt uitgevoerd. Ze beïnvloeden hoe vaak het automatische proces wordt uitgevoerd en de andere handmatig een opschoning in gang zet.

De vuilnisman kan worden gemanipuleerd door de inzameldrempels af te stemmen die van invloed zijn op de frequentie waarmee de collector werkt. Python maakt gebruik van een op generatie gebaseerd geheugenbeheersysteem. Nieuwe objecten worden opgeslagen in de nieuwste generatie - generatie0 en bij elke overgebleven collectie worden objecten gepromoveerd tot oudere generaties. Na het bereiken van de laatste generatie - generatie2 , worden ze niet langer gepromoot.

De drempels kunnen worden gewijzigd met behulp van het volgende fragment:

import gc
gc.set_threshold(1000, 100, 10) # Values are just for demonstration purpose

Het eerste argument vertegenwoordigt de drempel voor het verzamelen van generatie0 . Elke keer als het aantal toewijzingen groter dan het aantal deallocations met 1000 de garbage collector zal worden genoemd.

De oudere generaties worden niet bij elke run schoongemaakt om het proces te optimaliseren. De tweede en derde argumenten zijn optioneel en bepalen hoe vaak de oudere generaties worden opgeschoond. Als generatie0 100 keer is verwerkt zonder generatie 1 op te schonen, wordt generatie 1 verwerkt. Op dezelfde manier worden objecten in generatie 2 alleen verwerkt wanneer degene in generatie1 10 keer werden opgeschoond zonder generatie2 aan te raken.

Een voorbeeld waarbij het handmatig instellen van de drempels nuttig is, is wanneer het programma veel kleine objecten toewijst zonder ze te dealloceren, wat ertoe leidt dat de vuilnisman te vaak wordt uitgevoerd (toewijzingen van elke generatie 0_threshold- objecten). Hoewel de verzamelaar behoorlijk snel is, levert het een enorm probleem op wanneer het op een groot aantal objecten draait. Hoe dan ook, er is geen one size fits all-strategie voor het kiezen van de drempels en het is use case betrouwbaar.

Handmatig een verzameling activeren kan als volgt in het volgende fragment:

import gc
gc.collect()

De afvalinzameling wordt automatisch geactiveerd op basis van het aantal toewijzingen en deallocaties, niet op basis van het verbruikte of beschikbare geheugen. Als u met grote objecten werkt, kan het geheugen daarom uitgeput raken voordat de automatische opschoning wordt geactiveerd. Dit is een goed gebruik om de vuilnisman handmatig te bellen.

Hoewel het mogelijk is, is het geen aangemoedigde oefening. Geheugenlekken voorkomen is de beste optie. Hoe dan ook, in grote projecten kan het detecteren van geheugenlekken een zware taak zijn en kan het handmatig activeren van een afvalinzameling worden gebruikt als een snelle oplossing tot verdere foutopsporing.

Voor langlopende programma's kan de afvalinzameling worden geactiveerd op basis van tijd of op basis van gebeurtenissen. Een voorbeeld voor de eerste is een webserver die een verzameling activeert na een vast aantal aanvragen. Voor de laatste, een webserver die een afvalinzameling activeert wanneer een bepaald type aanvraag wordt ontvangen.

Wacht niet tot de afvalinzameling is opgeruimd

Het feit dat de afvalinzameling zal opruimen, betekent niet dat je moet wachten tot de afvalinzamelingcyclus is opgeruimd.

In het bijzonder moet u niet wachten tot garbage collection de bestandshandvatten, databaseverbindingen en open netwerkverbindingen sluit.

bijvoorbeeld:

In de volgende code gaat u ervan uit dat het bestand wordt gesloten bij de volgende afvalinzamelcyclus, als f de laatste verwijzing naar het bestand was.

>>> f = open("test.txt")
>>> del f

Een meer expliciete manier om op te ruimen is door f.close() aan te roepen. Je kunt het nog eleganter doen, dat wil zeggen door de instructie with te gebruiken, ook wel de contextmanager genoemd :

>>> with open("test.txt") as f:
...     pass
...     # do something with f
>>> #now the f object still exists, but it is closed

Met de instructie with kunt u uw code onder het geopende bestand laten inspringen. Dit maakt het expliciet en gemakkelijker om te zien hoe lang een bestand open blijft. Het sluit ook altijd een bestand, zelfs als er een uitzondering wordt gemaakt in het while blok.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow