← Назад к вопросам

Как работает очистка поколений в сборщике мусора (Garbage collector)?

3.0 Senior🔥 151 комментариев
#Python Core#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работает очистка поколений в сборщике мусора (Garbage collector)

Python использует гибридный подход: подсчёт ссылок (reference counting) + сборка мусора по поколениям (generational GC). Это один из самых сложных механизмов в Python.

1. Подсчёт ссылок (Reference Counting)

Основной механизм очистки памяти в Python:

import sys

# Каждый объект имеет счётчик ссылок
a = []
print(sys.getrefcount(a))  # 2 (одна ссылка a + одна для аргумента getrefcount)

b = a  # +1 ссылка
print(sys.getrefcount(a))  # 3

c = a  # +1 ссылка
print(sys.getrefcount(a))  # 4

del b  # -1 ссылка
print(sys.getrefcount(a))  # 3

del c  # -1 ссылка
print(sys.getrefcount(a))  # 2

del a  # -1 ссылка, счётчик = 1, объект удаляется
# Объект удалён из памяти

Проблема: циклические ссылки не удаляются:

class Node:
    def __init__(self):
        self.ref = self  # циклическая ссылка

node = Node()
print(sys.getrefcount(node))  # 2

del node  # Счётчик = 1, но объект не удаляется!
# Объект остаётся в памяти

2. Поколения в сборщике мусора

Python использует 3 поколения для обнаружения циклических ссылок:

import gc

# Проверь текущее состояние GC
print(gc.get_count())  # (objects_gen0, objects_gen1, objects_gen2)
# Например: (500, 10, 5)

# Пороги для автоматической сборки
print(gc.get_threshold())  # (700, 10, 10)
# Gen0 собирается каждые 700 создаваемых объектов
# Gen1 собирается после 10 сборок Gen0
# Gen2 собирается после 10 сборок Gen1

# Изменение порогов
gc.set_threshold(1000, 15, 15)

Как это работает:

  • Поколение 0: новые объекты, собирается часто
  • Поколение 1: объекты, пережившие 1 сборку Gen0
  • Поколение 2: объекты, пережившие 1 сборку Gen1 (долгоживущие)
# Жизненный цикл объекта
obj = []  # Добавляется в Gen0
# Поколение 0 переполняется -> сборка мусора
#   obj выживает -> переходит в Gen1
# Поколение 1 переполняется -> сборка мусора
#   obj выживает -> переходит в Gen2
# Gen2 собирается редко (долгоживущие объекты)

3. Алгоритм Mark-and-Sweep для циклических ссылок

# Пример циклической ссылки
class Node:
    def __init__(self, value):
        self.value = value
        self.next = None

# Создаём цикл
node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1  # Циклическая ссылка

# Счётчик ссылок:
# node1: 2 ссылки (переменная + node2.next)
# node2: 2 ссылки (переменная + node1.next)

# Если удалить переменные:
del node1, node2
# Счётчики обе = 1, но объекты не удаляются!
# Это задача для сборщика мусора

# Сборщик мусора:
# 1. MARK фаза: найти все достижимые объекты из корней
# 2. SWEEP фаза: удалить недостижимые объекты
import gc
gc.collect()  # Принудительный сбор мусора
# Объекты node1 и node2 удаляются здесь

4. Управление сборщиком мусора

import gc
import sys

# Отключение автоматического сбора мусора
gc.disable()
print(gc.isenabled())  # False

# Твой код
for i in range(1000000):
    x = []
    y = x
    del x, y
# Память не очищается!

# Принудительная сборка мусора
gc.collect()
print(gc.isenabled())  # False

# Включение автоматического сбора мусора
gc.enable()
print(gc.isenabled())  # True

5. Отслеживание сборки мусора

import gc

# Включение отладки
gc.set_debug(gc.DEBUG_SAVEALL)

# Сборка мусора
gc.collect()

# Посмотри, что было собрано
print(f"Garbage collected: {len(gc.garbage)} objects")
for obj in gc.garbage[:10]:
    print(type(obj))

# Отключение отладки
gc.set_debug(0)

6. Поколения в действии

import gc

class Tracker:
    def __init__(self, gen):
        self.generation = gen
    
    def __del__(self):
        print(f"Object from generation {self.generation} deleted")

# Отслеживание
gc.collect()
print(f"GC threshold: {gc.get_threshold()}")

# Создание объектов в Gen0
objs_gen0 = [Tracker(0) for _ in range(100)]
print(f"Count: {gc.get_count()}")  # Gen0 растёт

# Достижение порога Gen0
while gc.get_count()[0] < 700:
    x = []

print("Gen0 полна, сборка мусора...")
gc.collect()  # Принудительная сборка

# Объекты перейдут в Gen1
print(f"Count after collection: {gc.get_count()}")

7. Оптимизация производительности

# Проблема: частая сборка мусора замораживает приложение
import time
import gc

# Профилирование сборки мусора
start = time.time()
for i in range(1000000):
    x = []
end = time.time()
print(f"With GC: {end - start:.2f}s")

# Оптимизация: отключение GC на время критичного кода
gc.disable()
start = time.time()
for i in range(1000000):
    x = []
end = time.time()
print(f"Without GC: {end - start:.2f}s")  # Намного быстрее

# Сборка после критичного кода
gc.collect()
gc.enable()

# Увеличение порогов для больших приложений
gc.set_threshold(10000, 15, 15)

8. Утечки памяти и отладка

import gc

# Найти объекты, на которые ничто не ссылается
def find_cycles():
    gc.collect()
    unreachable = gc.garbage[:]
    return unreachable

# Посмотри, что не собирается
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()

if gc.garbage:
    print(f"Found {len(gc.garbage)} unreachable objects")
    for obj in gc.garbage[:5]:
        print(f"Type: {type(obj)}, Size: {sys.getsizeof(obj)}")

# Отключение отладки
gc.set_debug(0)

9. Проблема: del методы

class Resource:
    def __init__(self):
        self.data = b"X" * 1000000
    
    def __del__(self):
        print("Cleaning up...")
        # __del__ вызывается при удалении объекта

# Проблема: циклические ссылки с __del__
class Node:
    def __init__(self):
        self.next = None
        self.resource = Resource()
    
    def __del__(self):
        print("Node deleted")

node1 = Node()
node2 = Node()
node1.next = node2
node2.next = node1  # Циклическая ссылка

del node1, node2
# Сборщик мусора НЕ может удалить эти объекты!
# Потому что не знает, в каком порядке вызывать __del__

# Решение: использовать weakref
import weakref

class SafeNode:
    def __init__(self):
        self._next = None
    
    @property
    def next(self):
        return self._next() if self._next else None
    
    @next.setter
    def next(self, value):
        self._next = weakref.ref(value) if value else None

10. Лучшие практики

  • Избегай циклических ссылок — используй weakref если необходимо
  • Не полагайся на del — используй контекстные менеджеры (with)
  • Отключай GC для критичного кода — если нужна максимальная производительность
  • Профилируй утечки памяти — tracemalloc и gc.garbage помогут
  • Используй контекстные менеджеры — обеспечивают детерминированную очистку:
# Хорошо
with open('file.txt') as f:
    data = f.read()
# Файл гарантированно закроется, не зависит от GC

# Плохо
f = open('file.txt')
data = f.read()
# Файл может остаться открытым, пока не запустится GC

Помни: поколения оптимизированы для типичного поведения объектов — новые объекты часто удаляются быстро, долгоживущие объекты редко участвуют в сборке.

Как работает очистка поколений в сборщике мусора (Garbage collector)? | PrepBro