programowanieProgramista Backend

Wyjaśnij, jak działa zarządzanie pamięcią w Pythonie, w tym zbieracz śmieci. Czym są odniesienia cykliczne i jak Python sobie z nimi radzi?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź

W Pythonie pamięć zarządzana jest automatycznie: przy tworzeniu obiektu pamięć jest alokowana, a gdy nie odnosi się do niego żaden inny obiekt, pamięć jest zwalniana. Główną strategią jest liczenie referencji: do każdego obiektu przypisany jest licznik referencji, gdy staje się zerowy — obiekt zostaje usunięty.

Jednak mogą występować odniesienia cykliczne, gdy obiekty odwołują się do siebie nawzajem i ich licznik referencji nigdy nie spada do zera, nawet jeśli z zewnętrznego kodu odniesienia zostały usunięte.

W celu walki z tym w Pythonie (CPython) istnieje zbieracz śmieci (garbage collector, GC), który wykrywa cykle obiektów, które nie są dostępne z korzeni i je usuwa. Zarządzanie zbieraczem śmieci można przeprowadzać z modułu gc.

Przykład cyklu:

class Node: def __init__(self): self.ref = None n1 = Node() n2 = Node() n1.ref = n2 n2.ref = n1 del n1, n2 # obiekty wciąż żyją z powodu odniesienia cyklicznego!

Podchwytliwe pytanie

Pytanie: „Czy wywołanie del obj zawsze natychmiast zwolni pamięć?”

Odpowiedź: Nie! Operacja del tylko usuwa jedno odniesienie do obiektu. Jeśli gdzieś (jawnie lub cyklicznie) pozostają inne odniesienia — pamięć nie zostanie zwolniona, dopóki licznik referencji nie osiągnie zera i/lub GC nie pozbędzie się cykli. Przykład:

import gc class A: pass x = A() y = x del x # y wciąż ma odniesienie, obiekt żyje! del y # dopiero teraz obiekt może zostać usunięty

Przykłady rzeczywistych błędów z powodu nieznajomości niuansów tematu


Historia

W systemie przetwarzania dużych plików trzymano w pamięci powiązane obiekty. Po usunięciu odniesień zewnętrznych pamięć wciąż nie była zwalniana. Przyczyna — odniesienia cykliczne, które nie były od razu czyszczone, a czasami w ogóle nie były usuwane z powodu obecności destruktora __del__.


Historia

W długoterminowym procesie serwerowym często tworzone były duże tymczasowe struktury z cyklicznymi grafami. Programiści nie wyłączyli tymczasowego wyjścia debugowania GC, przez co występowały rzadkie, ale odczuwalne przestoje w tle. Diagnozowano to jako "spadki wydajności". Okazało się, że winny był częsty uruchamianie zbieracza śmieci.


Historia

Przy przenoszeniu dużego projektu na PyPy (alternatywny interpreter) stary kod polegał na specyficznym zachowaniu CPython polegającym na natychmiastowym czyszczeniu pamięci przez licznik referencji — w PyPy obiekty były usuwane i czyszczone nie od razu, lecz "na życzenie" GC, co prowadziło do nieprzewidywalnego zachowania w przypadku otwartych plików i połączeń sieciowych.