Python Language
Skräp samling
Sök…
Anmärkningar
I kärnan är Pythons skräpkollektor (från och med 3.5) en enkel implementering av referensräknare. Varje gång du gör en referens till ett objekt (till exempel a = myobject
) a = myobject
referensräkningen på det objektet (myobject). Varje gång en referens tas bort, minskas referensräkningen, och när referensräkningen når 0
, vet vi att ingenting har en referens till det objektet och vi kan omlokalisera det!
En vanlig missförståelse om hur Python-minneshanteringen fungerar är att nyckelordet del
frigör objektminne. Det är inte sant. Det som faktiskt händer är att del
nyckelordet bara minskar objekten refcount, vilket innebär att om du kallar det tillräckligt många gånger för att återbetalningen ska nå noll kan objektet samlas in i skräp (även om det fortfarande finns referenser till objektet som finns tillgängligt någon annanstans i din kod ).
Python skapar eller rensar aggresivt objekt första gången den behöver dem. Om jag utför uppdraget a = objekt () tilldelas minnet för objektet vid den tiden (cpython kommer ibland att återanvända vissa typer av objekt, t.ex. listor under huven, men för det mesta håller den inte en fri objektpool och utför allokering när du behöver det). På samma sätt, så snart avräkningen har minskats till 0, stänger GC upp det.
Generational Garbage Collection
På 1960-talet upptäckte John McCarthy en dödlig brist i att återrapportera skräpsamlingen när han implementerade den återkalkyleringsalgoritm som Lisp använde: Vad händer om två objekt hänvisar till varandra i en cyklisk referens? Hur kan du någonsin skräp samla dessa två objekt även om det inte finns några externa referenser till dem om de alltid kommer att hänvisa till varandra? Detta problem sträcker sig också till alla cykliska datastrukturer, såsom en ringbuffert eller två på varandra följande poster i en dubbel länkad lista. Python försöker fixa detta problem med hjälp av en lite intressant twist på en annan algoritm för skräppsamling som heter Generational Garbage Collection .
I huvudsak, varje gång du skapar ett objekt i Python lägger det till slutet av en dubbel länkad lista. Ibland går Python igenom den här listan, kontrollerar vilka objekt objekten i listan också hänvisar till, och om de också finns i listan (vi ser varför de kanske inte är i ett ögonblick), minskar deras rabatter ytterligare. Vid denna punkt (faktiskt finns det några heuristik som avgör när saker flyttas, men låt oss anta att det är efter en enda samling för att hålla sakerna enkla) allt som fortfarande har en rabatt större än 0 blir befordrad till en annan länkad lista som kallas "Generation 1" (det är därför alla objekt inte alltid finns i generationens 0-lista) som har den här slingan tillämpad mindre ofta. Det är här generationsavfallssamlingen kommer in. Det finns tre generationer som standard i Python (tre länkade objektlistor): Den första listan (generation 0) innehåller alla nya objekt; om en GC-cykel inträffar och objekten inte samlas, flyttas de till den andra listan (generation 1), och om en GC-cykel inträffar på den andra listan och de fortfarande inte samlas flyttas de till den tredje listan (generation 2 ). Den tredje generationens lista (kallas "generation 2", eftersom vi är noll indexering) är skräp som samlas in mycket mindre ofta än de första två, idén är att om ditt objekt är långlivat är det inte lika sannolikt att GCed, och kanske aldrig vara GCed under din ansöknings livstid så det är ingen mening att slösa tid på att kontrollera det på varje GC-körning. Dessutom observeras att de flesta föremål är skräp som samlas in relativt snabbt. Från och med nu kommer vi att kalla dessa "goda föremål" eftersom de dör unga. Detta kallas den "svaga generationshypotesen" och observerades också först på 60-talet.
Snabbt åt sidan: till skillnad från de två första generationerna är den långlivade tredje generationens lista inte skräp som samlas in på ett vanligt schema. Det kontrolleras när förhållandet mellan länge väntande objekt (de som finns i tredje generationens lista, men inte har haft en GC-cykel ännu) och det totala långlivade objektet i listan är större än 25%. Detta beror på att den tredje listan är obegränsad (saker flyttas aldrig bort från den till en annan lista, så de försvinner bara när de faktiskt är skräp samlade), vilket betyder att för applikationer där du skapar massor av långlivade objekt, GC-cykler på den tredje listan kan bli ganska lång. Genom att använda ett förhållande uppnår vi "amorterad linjär prestanda i det totala antalet objekt"; aka, ju längre listan är, desto längre tid tar GC, men desto mindre ofta utför vi GC (här är det ursprungliga förslaget från 2008 för detta heuristik av Martin von Löwis för vidare läsning). Handlingen att utföra en sopor samling på tredje generationen eller "mogna" listan kallas "fullständig sopor samling".
Så generationsavfallssamlingen påskyndar saker och ting genom att inte kräva att vi skannar över föremål som troligtvis inte behöver GC hela tiden, men hur hjälper det oss att bryta cykliska referenser? Förmodligen inte särskilt bra, visar det sig. Funktionen för att faktiskt bryta dessa referenscykler börjar så här :
/* 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)
Anledningen till att generationsavfallssamlingen hjälper till med detta är att vi kan behålla listans längd som en separat räkning; varje gång vi lägger till ett nytt objekt till generationen ökar vi det här antalet, och varje gång vi flyttar ett objekt till en annan generation eller delar upp det, minskar vi räkningen. Teoretiskt i slutet av en GC-cykel bör detta antal (för de två första generationerna i alla fall) alltid vara 0. Om det inte är, är något i listan som finns kvar någon form av cirkulär referens och vi kan släppa den. Men det finns ytterligare ett problem här: Tänk om de återstående objekten har Pythons magiska metod __del__
på dem? __del__
kallas när som helst ett Python-objekt förstörs. Men om två objekt i en cirkulär referens har __del__
metoder, kan vi inte vara säkra på att förstöra ett inte kommer att bryta den andra __del__
metoden. För ett förfalskat exempel, föreställ dig att vi skrev följande:
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)
och vi ställer in en instans av A och en instans av B för att peka på varandra och sedan hamnar de i samma skräppassningscykel? Låt oss säga att vi väljer en slumpmässig och delar vår förekomst av A först; A: s __del__
metod kommer att kallas, den kommer att skrivas ut, sedan kommer A att frigöras. Nästa vi kommer till B, vi kallar __del__
metoden, och oj! Segfault! A finns inte längre. Vi kan fixa detta genom att ringa allt som finns kvar på __del__
metoderna först, sedan göra ett nytt pass för att faktiskt deallocera allt, men detta introducerar ett annat, problem: Vad om en objekt __del__
metod sparar en referens till det andra objektet som håller på att bli GCed och har en hänvisning till oss någon annanstans? Vi har fortfarande en referenscykel, men nu är det inte möjligt att faktiskt GC antingen objekt, även om de inte längre används. Observera att även om ett objekt inte ingår i en cirkulär datastruktur, kan det återuppliva sig i sin egen __del__
metod; Python har en kontroll av detta och kommer att stoppa GCing om ett objekt som återbetalas har ökat efter dess __del__
metod har kallats.
CPython hanterar detta är genom att fästa de obegripliga objekten (något med någon form av cirkulär referens och en __del__
metod) på en global lista med oupphämtningsbart skräp och sedan lämna det där i all evighet:
/* list of uncollectable objects */
static PyObject *garbage = NULL;
Referensräkning
Den stora majoriteten av Python-minneshanteringen hanteras med referensräkning.
Varje gång ett objekt refereras (t.ex. tilldelas en variabel) ökas dess referensantal automatiskt. När den avbryts (t.ex. variabeln går utanför räckvidden) minskas referensräkningen automatiskt.
När referensräkningen når noll förstörs objektet omedelbart och minnet frigörs omedelbart. I de flesta fall behövs således inte skräpfångaren.
>>> 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
För att ytterligare demonstrera referensbegreppet:
>>> 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 för referenscykler
Den enda gången du behöver sopor är om du har en referenscykel . Det enkla exemplet på en referenscykel är ett där A hänvisar till B och B hänvisar till A, medan inget annat hänvisar till varken A eller B. Varken A eller B är tillgängliga var som helst i programmet, så att de kan säkert förstöras, ändå är deras referensräkning 1 och så att de inte kan frigöras endast med referensräknarealgoritmen.
>>> 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
En referenscykel kan vara godtycklig lång. Om A pekar på B pekar på C pekar på ... pekar på Z som pekar på A, kommer varken A till Z att samlas in förrän skräppassningsfasen:
>>> 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
Effekter av del-kommandot
Ta bort ett variabelnamn från räckvidden med hjälp av del v
, eller ta bort ett objekt från en samling med del v[item]
eller del[i:j]
, eller ta bort ett attribut med del v.name
, eller på något annat sätt att ta bort referenser till ett objekt, inte utlöser några destructor samtal eller något minne frigörs i och av sig själv. Objekt förstörs endast när deras referensräkning når noll.
>>> 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
Återanvändning av primitiva föremål
En intressant sak att notera som kan hjälpa till att optimera dina applikationer är att primitiva faktiskt också redovisas under huven. Låt oss titta på siffror; för alla heltal mellan -5 och 256 återanvänder Python alltid samma objekt:
>>> import sys
>>> sys.getrefcount(1)
797
>>> a = 1
>>> b = 1
>>> sys.getrefcount(1)
799
Observera att återbetalningen ökar, vilket innebär att a
och b
refererar till samma underliggande objekt när de hänvisar till det 1
primitiva. Men för större antal återanvänder Python faktiskt inte det underliggande objektet:
>>> a = 999999999
>>> sys.getrefcount(999999999)
3
>>> b = 999999999
>>> sys.getrefcount(999999999)
3
Eftersom återbetalningen för 999999999
inte ändras när vi tilldelar den till a
och b
vi dra slutsatsen att de hänvisar till två olika underliggande objekt, även om de båda tilldelas samma primitiva.
Visar ett objekts antal
>>> import sys
>>> a = object()
>>> sys.getrefcount(a)
2
>>> b = a
>>> sys.getrefcount(a)
3
>>> del b
>>> sys.getrefcount(a)
2
Fördelar kraftfullt med objekt
Du kan tvinga tilldelade objekt även om deras antal inte är 0 i både Python 2 och 3.
Båda versionerna använder ctypes
att göra det.
VARNING: Om du gör detta kommer din Python-miljö att bli instabil och benägen att krascha utan spårning! Om du använder den här metoden kan det också medföra säkerhetsproblem (ganska osannolikt) Endast omlokalisera objekt som du är säker på att du aldrig kommer att referera igen. Någonsin.
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
Efter att ha kört kommer någon hänvisning till det nu omlokaliserade objektet att Python antingen producerar odefinierat beteende eller kraschar - utan en traceback. Det fanns antagligen en anledning till att skräpfångaren inte tog bort det föremålet ...
Om du deallocate None
får du ett speciellt meddelande - Fatal Python error: deallocating None
innan du kraschar.
Hantera sopor
Det finns två sätt att påverka när en minnesrensning utförs. De påverkar hur ofta den automatiska processen utförs och den andra utlöser manuellt en sanering.
Sopkollektorn kan manipuleras genom att ställa in uppsamlingströsklarna som påverkar frekvensen vid vilken kollektorn körs. Python använder ett generationsbaserat minnehanteringssystem. Nya objekt sparas i den nyaste generationen - generation0 och med varje överlevd samling befordras objekt till äldre generationer. Efter att ha nått den sista generationen - generation2 , marknadsförs de inte längre.
Trösklarna kan ändras med följande utdrag:
import gc
gc.set_threshold(1000, 100, 10) # Values are just for demonstration purpose
Det första argumentet representerar tröskeln för att samla generation0 . Varje gång antalet tilldelningar överstiger antalet fördelningar med 1000 kommer avfallssamlaren att ringas.
De äldre generationerna städas inte vid varje körning för att optimera processen. Det andra och tredje argumentet är valfria och styr hur ofta de äldre generationerna rengörs. Om generation0 bearbetades 100 gånger utan rengöring generation1 , kommer generation1 att behandlas. På liknande sätt kommer objekt i generation 2 att behandlas endast när de i generation 1 städades 10 gånger utan att beröra generation2 .
Ett exempel där manuellt ställer in trösklarna är fördelaktigt är när programmet allokerar en hel del små föremål utan att omlokalisera dem vilket leder till att skräpkollektorn körs för ofta (varje generation0_threshold- objektallokeringar). Trots att samlaren är ganska snabb, när den körs på ett stort antal föremål, utgör den en prestationsproblem. Hur som helst, det finns ingen storlek som passar alla strategier för att välja tröskelvärden och det är användningsfall beroende.
Manuell triggning av en samling kan göras som i följande utdrag:
import gc
gc.collect()
Skräpkollektionen utlöses automatiskt baserat på antalet tilldelningar och fördelningar, inte på det konsumerade eller tillgängliga minnet. Följaktligen, när du arbetar med stora objekt, kan minnet tappas innan den automatiska saneringen utlöses. Detta är ett bra användningsfall för att manuellt ringa sopor.
Trots att det är möjligt är det inte en uppmuntrad praxis. Att undvika minnesläckor är det bästa alternativet. Hur som helst, i stora projekt kan detektera minnesläckan vara en fast uppgift och manuellt utlösa en soporinsamling kan användas som en snabb lösning tills ytterligare felsökning.
För långvariga program kan skräpkollektionen utlösas tidvis eller på händelsebasis. Ett exempel för den första är en webbserver som utlöser en samling efter ett fast antal förfrågningar. För det senare, en webbserver som utlöser en sopsamling när en viss typ av begäran tas emot.
Vänta inte på att soporna samlas in
Det faktum att skräpuppsamlingen kommer att rensa betyder inte att du bör vänta på att skräpuppsamlingscykeln rensas upp.
I synnerhet bör du inte vänta på att skräpuppsamlingen stänger filhandtag, databasanslutningar och öppnar nätverksanslutningar.
till exempel:
I följande kod antar du att filen kommer att stängas vid nästa skräppassningscykel, om f var den sista referensen till filen.
>>> f = open("test.txt")
>>> del f
Ett mer tydligt sätt att rensa upp är att ringa f.close()
. Du kan göra det ännu mer elegant, det vill säga genom att använda with
uttalandet, även känt som context manager :
>>> with open("test.txt") as f:
... pass
... # do something with f
>>> #now the f object still exists, but it is closed
Den with
uttalandet kan du indrag koden under öppna filen. Detta gör det tydligt och lättare att se hur länge en fil hålls öppen. Det stänger också alltid en fil, även om ett undantag tas upp i while
blocket.