Szukaj…


Uwagi

Zasadniczo moduł zbierający śmieci Pythona (od wersji 3.5) jest prostą implementacją liczenia referencji. Za każdym razem, gdy robisz odniesienie do obiektu (na przykład a = myobject ), liczba referencji na tym obiekcie (mój obiekt) jest zwiększana. Za każdym razem, gdy referencja jest usuwana, liczba referencji jest zmniejszana, a gdy liczba referencji osiągnie wartość 0 , wiemy, że nic nie zawiera referencji do tego obiektu i możemy ją zwolnić!

Jednym z powszechnych nieporozumień dotyczących działania zarządzania pamięcią w języku Python jest to, że słowo kluczowe del zwalnia pamięć obiektów. To nie jest prawda. To, co faktycznie się dzieje, polega na tym, że słowo kluczowe del jedynie zmniejsza liczenie obiektów, co oznacza, że jeśli wywołasz je wystarczająco długo, aby licznik osiągnął zero, obiekt może zostać wyrzucony na śmietnik (nawet jeśli faktycznie istnieją odwołania do obiektu dostępne w innym miejscu w kodzie ).

Python agresywnie tworzy lub czyści obiekty za pierwszym razem, gdy ich potrzebują Jeśli wykonam przypisanie a = object (), pamięć dla obiektu jest w tym czasie przydzielana (cpython czasami ponownie wykorzystuje określone typy obiektów, np. Listy pod maską, ale przeważnie nie utrzymuje wolnej puli obiektów i dokonuje alokacji, gdy jej potrzebujesz). Podobnie, gdy tylko liczba kont zostanie zmniejszona do 0, GC je wyczyści.

Generacyjne odśmiecanie

W latach sześćdziesiątych John McCarthy odkrył fatalną wadę w liczeniu śmieci, kiedy zaimplementował algorytm ponownego liczenia stosowany przez Lisp: Co się stanie, jeśli dwa obiekty będą się nawzajem odnosić w cyklicznym odwołaniu? Jak możesz śmieci wyrzucić te dwa obiekty, nawet jeśli nie ma do nich zewnętrznych odniesień, jeśli zawsze będą się do siebie nawiązywać? Problem ten dotyczy także dowolnej cyklicznej struktury danych, takiej jak bufory pierścieniowe lub dowolne dwa kolejne wpisy na podwójnie połączonej liście. Python próbuje rozwiązać ten problem, wykorzystując nieco interesujący zwrot w innym algorytmie odśmiecania zwanym Generacyjnym odśmiecaniem.

W zasadzie za każdym razem, gdy tworzysz obiekt w Pythonie, dodaje go on na końcu podwójnie połączonej listy. Czasami Python zapętla tę listę, sprawdza, do jakich obiektów odnoszą się również obiekty na liście, a jeśli one również znajdują się na liście (zobaczymy, dlaczego za chwilę ich nie będzie), dodatkowo zmniejsza swoje przeliczniki. W tym momencie (w rzeczywistości istnieją pewne heurystyki, które określają, kiedy rzeczy się przenoszą, ale załóżmy, że jest to po pojedynczej kolekcji, aby uprościć sprawy) wszystko, co nadal ma wartość refinansowania większą niż 0, jest promowane do innej połączonej listy o nazwie „Generacja 1” (dlatego wszystkie obiekty nie zawsze znajdują się na liście generacji 0), która rzadziej stosuje tę pętlę. W tym miejscu pojawia się generowanie śmieci. Generowane są w Pythonie 3 generacje (trzy połączone listy obiektów): Pierwsza lista (generacja 0) zawiera wszystkie nowe obiekty; jeśli zdarzy się cykl GC i obiekty nie zostaną zebrane, zostaną przeniesione na drugą listę (generacja 1), a jeśli cykl GC wydarzy się na drugiej liście i nadal nie zostaną zebrane, zostaną przeniesione na trzecią listę (generacja 2 ). Lista trzeciej generacji (zwana „generacją 2”, ponieważ indeksujemy zero) jest śmieciami zbierana znacznie rzadziej niż pierwsze dwie, przy założeniu, że jeśli twój obiekt jest długo żywy, nie jest tak prawdopodobne, że zostanie GCed i może nigdy być GCed przez cały okres użytkowania aplikacji, więc nie ma sensu tracić czasu na sprawdzanie go przy każdym uruchomieniu GC. Co więcej, zaobserwowano, że większość obiektów jest zbierana śmieci stosunkowo szybko. Odtąd będziemy nazywać te „dobrymi przedmiotami”, ponieważ umierają młodo. Nazywa się to „słabą hipotezą pokoleniową” i po raz pierwszy zaobserwowano ją w latach 60.

Krótko na bok: w przeciwieństwie do pierwszych dwóch pokoleń, długowieczna lista trzeciego pokolenia nie jest śmieciami zbierana zgodnie z harmonogramem. Jest sprawdzane, gdy stosunek długo żyjących obiektów oczekujących (tych, które są na liście trzeciej generacji, ale tak naprawdę nie miały jeszcze cyklu GC) do łącznej liczby obiektów długowiecznych na liście jest większy niż 25%. Wynika to z faktu, że trzecia lista jest nieograniczona (rzeczy nigdy nie są przenoszone z niej na inną listę, więc znikają tylko wtedy, gdy są faktycznie wyrzucane), co oznacza, że w aplikacjach, w których tworzysz wiele długo żyjących obiektów, cykle GC na trzeciej liście może być dość długa. Stosując współczynnik uzyskujemy „zamortyzowaną wydajność liniową w całkowitej liczbie obiektów”; aka, im dłuższa lista, tym dłużej trwa GC, ale tym rzadziej wykonujemy GC (oto oryginalna propozycja heurystyki z 2008 roku autorstwa Martina von Löwisa do dalszego czytania). Czynność wyrzucania elementów bezużytecznych z listy trzeciej generacji lub „dojrzałej” nazywa się „pełną funkcją wyrzucania elementów bezużytecznych”.

Tak więc generowanie śmieci w trybie pokoleniowym przyspiesza rzeczy ogromnie, nie wymagając, abyśmy skanowali obiekty, które prawdopodobnie nie będą potrzebować GC przez cały czas, ale jak to pomaga nam przełamać cykliczne odniesienia? Prawdopodobnie niezbyt dobrze, jak się okazuje. Funkcja faktycznego przerywania tych cykli odniesienia zaczyna się w następujący sposób :

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

Powodem, dla którego generowanie śmieci jest pomocne, jest to, że możemy zachować długość listy jako osobną liczbę; za każdym razem, gdy dodajemy nowy obiekt do generacji, zwiększamy tę liczbę, a za każdym razem, gdy przenosimy obiekt do innej generacji lub zwalniamy go, zmniejszamy liczbę. Teoretycznie na koniec cyklu GC liczba ta (w każdym razie przez pierwsze dwa pokolenia) powinna zawsze wynosić 0. Jeśli nie, wszystko na liście, które pozostało, jest jakąś formą okrągłego odniesienia i możemy to upuścić. Jest jednak jeszcze jeden problem: co jeśli pozostałe obiekty mają na sobie magiczną metodę Pythona __del__ ? __del__ jest wywoływany przy każdym zniszczeniu obiektu Python. Jeśli jednak dwa obiekty w odwołaniu cyklicznym mają metody __del__ , nie możemy być pewni, że zniszczenie jednego nie __del__ metody __del__ . Dla wymyślonego przykładu wyobraźmy sobie, że napisaliśmy:

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)

a my ustawiliśmy instancję A i instancję B, aby wskazywały na siebie, a następnie kończą w tym samym cyklu wyrzucania elementów bezużytecznych? Powiedzmy, że wybieramy jeden losowo i najpierw zwalniamy instancję A; __del__ zostanie metoda __del__ , zostanie wydrukowana, a następnie A zostanie zwolniona. Następnie dochodzimy do B, nazywamy jego metodę __del__ i ups! Segfault! Już nie istnieje. Możemy to naprawić, wywołując najpierw wszystko, co pozostało z metod __del__ , a następnie wykonując kolejne przejście, aby deallocować wszystko, jednak wprowadza to inny problem: Co jeśli jedna metoda __del__ zapisuje odwołanie do drugiego obiektu, który ma być GCed i ma odniesienie do nas gdzieś indziej? Nadal mamy cykl odniesienia, ale teraz nie jest możliwe GC żadnego obiektu, nawet jeśli nie są one już używane. Zauważ, że nawet jeśli obiekt nie jest częścią okrągłej struktury danych, może się odrodzić własną metodą __del__ ; Python sprawdza to i zatrzyma GCing, jeśli zwiększy się liczba __del__ po __del__ metody __del__ .

CPython radzi sobie z tym, przyklejając te obiekty __del__ GC (wszystko z pewną formą odwołania cyklicznego i metodą __del__ ) do globalnej listy nieściągalnych śmieci, a następnie pozostawiając je tam na całą wieczność:

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

Liczenie referencji

Zdecydowana większość zarządzania pamięcią w Pythonie jest obsługiwana przez liczenie referencji.

Za każdym razem, gdy obiekt jest wywoływany (np. Przypisywany do zmiennej), liczba odniesień jest automatycznie zwiększana. Gdy jest wyłuskiwany (np. Zmienna wychodzi poza zakres), liczba referencji jest automatycznie zmniejszana.

Gdy liczba odniesień osiągnie zero, obiekt jest natychmiast niszczony, a pamięć natychmiast uwalniana. Dlatego w większości przypadków śmieciarz nie jest nawet potrzebny.

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

Aby dodatkowo zademonstrować koncepcję referencji:

>>> 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 for Cycles referencyjne

Jedyny czas, w którym moduł śmieciowy jest potrzebny, to cykl referencyjny . Podobny przykład cyklu odniesienia to taki, w którym A odnosi się do B, a B odnosi się do A, podczas gdy nic więcej nie odnosi się do A lub B. Ani A, ani B nie są dostępne z dowolnego miejsca w programie, więc można je bezpiecznie zniszczyć, ale ich liczba referencyjna wynosi 1, więc nie można ich uwolnić za pomocą samego algorytmu zliczania referencji.

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

Cykl odniesienia może być dowolnie długi. Jeśli A wskazuje na B wskazuje na C wskazuje na ... wskazuje na Z, która wskazuje na A, to ani A do Z nie zostaną zebrane, aż do fazy wyrzucania elementów bezużytecznych:

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

Skutki polecenia del

Usuwanie nazwy zmiennej z zakresu za pomocą del v lub usuwanie obiektu z kolekcji za pomocą del v[item] lub del[i:j] , lub usuwanie atrybutu za pomocą del v.name lub w jakikolwiek inny sposób usuwania odniesień do obiekt, nie wyzwala żadnych wywołań destruktora ani zwalnia pamięci. Obiekty są niszczone tylko wtedy, gdy ich liczba referencyjna osiągnie zero.

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

Ponowne użycie prymitywnych obiektów

Interesującą rzeczą, na którą warto zwrócić uwagę, która może pomóc w optymalizacji aplikacji, jest fakt, że operacje podstawowe są również przeliczane pod maską. Spójrzmy na liczby; dla wszystkich liczb całkowitych od -5 do 256 Python zawsze używa tego samego obiektu:

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

Należy zauważyć, że wzrasta RefCount, tzn i a b odniesienie takie same bazowy obiekt, gdy odnoszą się one do 1 prymitywne. Jednak w przypadku większych liczb Python faktycznie nie wykorzystuje ponownie obiektu bazowego:

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

Ponieważ RefCount dla 999999999 nie zmienia się podczas przypisywania go do i a b możemy wywnioskować, że odnoszą się one do dwóch różnych obiektów bazowych, choć oboje są przypisane te same prymitywne.

Wyświetlanie przeliczenia obiektu

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

Zdecydowanie zwalniaj przedmioty

Możesz wymusić cofnięcie przydziału obiektów, nawet jeśli ich przelicznik nie jest równy 0 zarówno w Pythonie 2, jak i 3.

Obie wersje używają do tego modułu ctypes .

OSTRZEŻENIE: zrobienie tego spowoduje niestabilność środowiska Python i podatność na awarie bez śledzenia! Korzystanie z tej metody może również powodować problemy z bezpieczeństwem (dość mało prawdopodobne). Zwolnij tylko obiekty, na które już nigdy nie będziesz się odwoływać. Zawsze.

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

Po uruchomieniu każde odwołanie do obecnie zwolnionego obiektu spowoduje, że Python wytworzy niezdefiniowane zachowanie lub zawiesi się - bez śledzenia. Prawdopodobnie był powód, dla którego śmieciarz nie usunął tego obiektu ...

Jeśli cofniesz alokację None , pojawi się specjalny komunikat - Fatal Python error: deallocating None przed awarią.

Zarządzanie śmieciami

Istnieją dwa podejścia do wywierania wpływu na czyszczenie pamięci. Wpływają na to, jak często wykonywany jest proces automatyczny, a drugi ręcznie uruchamia czyszczenie.

Pojemnikiem na śmieci można manipulować, dostosowując progi zbierania, które wpływają na częstotliwość, z jaką działa kolektor. Python wykorzystuje system zarządzania pamięcią oparty na generacji. Nowe obiekty są zapisywane w najnowszej generacji - generacji0, a wraz z każdą zachowaną kolekcją obiekty są awansowane do starszych generacji. Po osiągnięciu ostatniej generacji - generacji 2 , nie są już promowane.

Progi można zmienić za pomocą następującego fragmentu:

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

Pierwszy argument reprezentuje próg gromadzenia generacji0 . Za każdym razem, gdy liczba przydziałów przekroczy liczbę zwolnień o 1000, wywoływany jest moduł wyrzucający elementy bezużyteczne.

Starsze generacje nie są czyszczone przy każdym uruchomieniu, aby zoptymalizować proces. Drugi i trzeci argument są opcjonalne i kontrolują częstotliwość czyszczenia starszych generacji. Jeśli generacja 0 została przetworzona 100 razy bez czyszczenia generacji 1 , wówczas generacja 1 zostanie przetworzona. Podobnie obiekty w generacji 2 będą przetwarzane tylko wtedy, gdy te w generacji 1 zostały wyczyszczone 10 razy bez dotykania generacji 2 .

Jednym z przykładów, w których ręczne ustawianie progów jest korzystne, jest to, że program przydziela wiele małych obiektów bez cofania ich przydzielenia, co prowadzi do zbyt częstego działania modułu wyrzucania elementów bezużytecznych (przydziały obiektów progowych dla każdego pokolenia0_0 ). Chociaż kolektor jest dość szybki, gdy działa na ogromnej liczbie obiektów, stanowi problem z wydajnością. W każdym razie nie ma jednego rozmiaru, który pasuje do wszystkich strategii wyboru progów, a jego użycie zależy od przypadku.

Ręczne wyzwalanie kolekcji można wykonać jak w następującym fragmencie:

import gc
gc.collect()

Wyrzucanie elementów bezużytecznych jest uruchamiane automatycznie na podstawie liczby przydziałów i zwolnień, a nie na zajętej lub dostępnej pamięci. W związku z tym podczas pracy z dużymi obiektami pamięć może ulec wyczerpaniu przed uruchomieniem automatycznego czyszczenia. To dobry przypadek użycia do ręcznego wywoływania śmietnika.

Chociaż jest to możliwe, nie jest to zalecana praktyka. Najlepszym rozwiązaniem jest unikanie wycieków pamięci. Tak czy inaczej, w dużych projektach wykrycie wycieku pamięci może być trudnym zadaniem, a ręczne uruchomienie wyrzucania elementów bezużytecznych można wykorzystać jako szybkie rozwiązanie do czasu dalszego debugowania.

W przypadku długo działających programów wyrzucanie elementów bezużytecznych można uruchomić na podstawie czasu lub zdarzenia. Przykładem pierwszego jest serwer WWW, który uruchamia kolekcję po ustalonej liczbie żądań. Później serwer WWW, który wyzwala odśmiecanie po odebraniu określonego typu żądania.

Nie czekaj, aż śmieci zostaną wyczyszczone

Fakt, że czyszczenie pamięci zostanie wyczyszczone, nie oznacza, że powinieneś poczekać na jego wyczyszczenie.

W szczególności nie należy czekać, aż wyrzucanie elementów bezużytecznych zamknie uchwyty plików, połączenia z bazą danych i otworzy połączenia sieciowe.

na przykład:

W poniższym kodzie założono, że plik zostanie zamknięty w następnym cyklu odśmiecania, jeśli f było ostatnim odwołaniem do pliku.

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

Bardziej wyraźnym sposobem na oczyszczenie jest wywołanie f.close() . Możesz to zrobić jeszcze bardziej elegancko, czyli używając instrukcji with , znanej również jako menedżer kontekstu :

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

Instrukcja with umożliwia wcięcie kodu w otwartym pliku. Ułatwia to określenie, jak długo plik jest otwarty. Zawsze również zamyka plik, nawet jeśli w bloku while pojawi się wyjątek.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow