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

Как решается кольцевая зависимость в сборщике мусора (Garbage collector)?

3.0 Senior🔥 81 комментариев
#Python Core

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

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

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

Кольцевая зависимость в сборщике мусора: проблема циклических ссылок

Кольцевая зависимость (circular reference) — это ситуация, когда объекты ссылаются друг на друга, создавая цикл. Сборщик мусора должен уметь выявлять и удалять такие объекты. В Python эта проблема решается с помощью многоуровневого подхода: счётчик ссылок + cycle detector.

Проблема циклических ссылок

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

# Создаём цикл
a = Node(1)
b = Node(2)
c = Node(3)

a.next = b
b.next = c
c.next = a  # Цикл! c → a → b → c

# Теперь удаляем переменные
del a, b, c

# Проблема: объекты остаются в памяти потому что
# они ссылаются друг на друга!
# a → next → b → next → c → next → a

Как Python решает циклические зависимости

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

Каждый объект в Python имеет счётчик ссылок (refcount).

import sys

class Node:
    def __init__(self, value):
        self.value = value

a = Node(1)
print(sys.getrefcount(a))  # 2 (одна в переменной a, одна в функции getrefcount)

b = a  # Теперь на него ссылаются две переменные
print(sys.getrefcount(a))  # 3

del b
print(sys.getrefcount(a))  # 2

# Когда refcount == 0 → объект удаляется

Проблема: при циклических ссылках refcount никогда не становится 0!

a = Node(1)
b = Node(2)

a.next = b
b.next = a  # Цикл

# refcount(a) = 2: переменная a + поле b.next
# refcount(b) = 2: переменная b + поле a.next

del a, b

# Теперь:  refcount(a) = 1 (из b.next)
#          refcount(b) = 1 (из a.next)
# Они никогда не удалятся! Memory leak!

2. Cycle Detector (Детектор циклов)

Питон имеет второй механизм — cycle detector, который периодически проверяет на циклы.

import gc

# Детектор циклов запускается автоматически или можно вызвать вручную
gc.collect()  # Запускает сборку мусора с поиском циклов

# Как это работает?
# 1. gc.collect() начинает процесс сборки
# 2. Проходит по всем объектам
# 3. Проверяет refcount каждого
# 4. Если объект часть цикла (refcount > внешних ссылок) → удаляет

Алгоритм поиска циклов (Simplified)

def garbage_collection_simplified():
    # Шаг 1: Отметить все объекты как "живые"
    living_objects = set(all_objects)
    
    # Шаг 2: Провести from tracing
    # Начиная с корней (global vars, local vars, стек),
    # отметить все достижимые объекты
    reachable = set()
    
    def mark_reachable(obj):
        if obj in reachable:
            return
        reachable.add(obj)
        # Рекурсивно отмечаем все на которые ссылается obj
        for child in get_referred_objects(obj):
            mark_reachable(child)
    
    # Начинаем с корней
    for root in [globals(), locals()]:
        mark_reachable(root)
    
    # Шаг 3: Удалить неживые объекты
    unreachable = living_objects - reachable
    for obj in unreachable:
        delete(obj)  # Вызывает __del__ если существует

Generational Garbage Collection (Поколенческая сборка)

Питон использует поколенческий подход для оптимизации:

import gc

# Python разделяет объекты на 3 поколения
# Поколение 0: молодые объекты (проверяется часто)
# Поколение 1: объекты среднего возраста
# Поколение 2: старые объекты (проверяется редко)

# Получить статистику
stats = gc.get_stats()
for gen in range(len(stats)):
    print(f"Generation {gen}: {stats[gen]['collected']} collected")

# Почему это работает?
# Наблюдение: молодые объекты часто становятся мусором
#            старые объекты обычно живут долго
# Поэтому проверяем молодые часто, старые редко → экономия времени

Пример с Python объектами

class Parent:
    def __init__(self, name):
        self.name = name
        self.child = None
    
    def __del__(self):
        print(f"Parent {self.name} deleted")

class Child:
    def __init__(self, name):
        self.name = name
        self.parent = None
    
    def __del__(self):
        print(f"Child {self.name} deleted")

# Создаём цикл
parent = Parent("Alice")
child = Child("Bob")

parent.child = child
child.parent = parent  # Цикл!

print("Before del")
del parent, child
print("After del, before gc.collect()")

# Объекты ещё не удалены!
# __del__ не был вызван

import gc
print("Calling gc.collect()...")
gc.collect()

# Теперь выведет:
# "Parent Alice deleted"
# "Child Bob deleted"

print("After gc.collect()")

Управление сборкой мусора

import gc

# Включить/отключить автоматическую сборку
gc.disable()   # Отключить автоматическую сборку (опасно!)
gc.enable()    # Включить

# Вручную запустить сборку
gc.collect()   # Собрать все поколения

# Получить список мусора
gc.garbage  # Список объектов, которые не могли быть удалены

# Получить информацию об объекте
import sys
obj = {"key": "value"}
referrers = gc.get_referrers(obj)  # Кто на него ссылается
referents = gc.get_referents(obj)  # На кого он ссылается

Проблемы и оптимизация

1. Объекты с del и циклы

Есть edge case: если объект имеет del и участвует в цикле, Python не знает в каком порядке вызывать del.

class A:
    def __del__(self):
        print("A.__del__")

class B:
    def __del__(self):
        print("B.__del__")

a = A()
b = B()

a.ref = b
b.ref = a  # Цикл

del a, b
import gc
# gc.collect() может вывести, может не вывести __del__ вызовы
# Потому что порядок не гарантирован

2. Производительность

Частый gc.collect() тратит CPU. Нужно балансировать:

# По умолчанию Python коллект автоматически когда:
# - Объектов поколения 0 > threshold0
# - Объектов поколения 1 > threshold1
# - Объектов поколения 2 > threshold2

print(gc.get_threshold())  # (700, 10, 10) по умолчанию

# Если критична производительность
gc.set_threshold(1000, 15, 15)  # Реже собирать

# Или собирать только критичные данные
gc.collect(0)  # Собрать только поколение 0

3. WeakRef для избежания циклов

Альтернатива: использовать слабые ссылки (weak references)

import weakref

class Parent:
    def __init__(self, name):
        self.name = name
        self.child = None

class Child:
    def __init__(self, name):
        self.name = name
        self.parent = None

parent = Parent("Alice")
child = Child("Bob")

parent.child = child
# Используем weakref вместо обычной ссылки
child.parent = weakref.ref(parent)

del parent, child
# Теперь объекты удалятся правильно
# Потому что нет цикла (weakref не считается обычной ссылкой)

Заключение

Питон решает проблему циклических ссылок используя двухуровневый подход:

  1. Reference Counting: удаляет объекты когда refcount = 0 (работает для большинства)
  2. Cycle Detector: периодически проверяет и удаляет циклы
  3. Generational GC: оптимизирует сборку мусора разделением объектов на поколения

Для критичного кода можно использовать weakref (слабые ссылки) чтобы избежать циклов вообще.