← Назад к вопросам
Как сборщик мусора работает с циклическими ссылками?
2.7 Senior🔥 121 комментариев
#Python Core
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Циклические ссылки и сборщик мусора в Python: Глубокий анализ
Это одна из самых интересных и важных тем в Python. Я разбирался с утечками памяти из-за циклических ссылок и вот что я выучил.
Проблема: Циклические ссылки
# Простая циклическая ссылка
class Node:
def __init__(self, value):
self.value = value
self.next = None
node_a = Node('A')
node_b = Node('B')
# Создаём цикл
node_a.next = node_b
node_b.next = node_a
# Удаляем ссылки
del node_a
del node_b
# Проблема: Объекты всё ещё в памяти, потому что ссылаются друг на друга!
Как работает подсчёт ссылок (Reference Counting)
Python использует простой механизм — подсчёт ссылок. Каждый объект хранит счётчик ссылок.
import sys
class Person:
def __init__(self, name):
self.name = name
alice = Person('Alice')
print(sys.getrefcount(alice)) # 2 (одна в переменной, одна в getrefcount)
bob = alice
print(sys.getrefcount(alice)) # 3 (две переменные + getrefcount)
friends = [alice]
print(sys.getrefcount(alice)) # 4 (alice, bob, список, getrefcount)
del alice
print(sys.getrefcount(bob)) # 3 (осталось bob, список, getrefcount)
# Когда счётчик = 0, объект удаляется немедленно
Проблема с циклическими ссылками
import sys
import gc
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __del__(self):
print(f"Node {self.name} удалён")
node_a = Node('A')
node_b = Node('B')
print(f"До цикла: A refcount = {sys.getrefcount(node_a) - 1}") # 1
node_a.next = node_b
node_b.next = node_a
print(f"После цикла: A refcount = {sys.getrefcount(node_a) - 1}") # 2
del node_a
del node_b
print("Объекты удалены?")
# Вывод: Они НЕ удалены! Циклические ссылки предотвращают их удаление.
Решение: Сборщик мусора (Garbage Collector)
Python имеет встроенный сборщик мусора для обработки циклических ссылок.
import gc
import sys
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __del__(self):
print(f"Node {self.name} удалён")
# Отключим автоматический сборщик для демонстрации
gc.disable()
node_a = Node('A')
node_b = Node('B')
node_a.next = node_b
node_b.next = node_a
del node_a
del node_b
print("После del, но до gc.collect():")
print(f"Количество объектов в gc: {len(gc.get_objects())}")
# Запускаем сборщик мусора
print("\nЗапускаю gc.collect()...")
gc.collect()
print("После gc.collect():")
print(f"Количество объектов в gc: {len(gc.get_objects())}")
# Включим сборщик обратно
gc.enable()
Как работает сборщик мусора в Python
Шаг 1: Обнаружение циклов
import gc
import weakref
class Node:
def __init__(self, name):
self.name = name
self.next = None
node_a = Node('A')
node_b = Node('B')
node_a.next = node_b
node_b.next = node_a
print("До удаления:")
print(f"node_a.next is node_b: {node_a.next is node_b}")
print(f"node_b.next is node_a: {node_b.next is node_a}")
# Объекты находятся в одном из трёх поколений сборщика
print(f"\nОбъекты в поколении 0: {len(gc.get_objects())}")
del node_a
del node_b
print(f"\nПосле del - объекты всё ещё в памяти!")
print(f"Объекты в поколении 0: {len(gc.get_objects())}")
Шаг 2: Трёхпоколенческая схема
Python использует поколения для оптимизации:
import gc
# Получить текущие пороги
thresholds = gc.get_threshold()
print(f"Пороги сборки мусора: {thresholds}")
# Output: (700, 10, 10) по умолчанию
# Поколение 0: 700 новых объектов → сборка
# Поколение 1: 10 сборок поколения 0 → сборка поколения 1
# Поколение 2: 10 сборок поколения 1 → сборка поколения 2
# Статистика сборок
stats = gc.get_stats()
print(f"\nСтатистика сборщика мусора:")
for i, stat in enumerate(stats):
print(f"Поколение {i}:")
print(f" Количество сборок: {stat['collections']}")
print(f" Собрано объектов: {stat['collected']}")
print(f" Несобираемых объектов: {stat['uncollectable']}")
Практический пример: Слабые ссылки (Weak References)
Для предотвращения циклов используй слабые ссылки.
import weakref
import gc
class Node:
def __init__(self, name):
self.name = name
self.next = None
def __del__(self):
print(f"Node {self.name} удалён")
node_a = Node('A')
node_b = Node('B')
# НЕПРАВИЛЬНО: Циклические ссылки
node_a.next = node_b
node_b.next = node_a
del node_a, node_b
# Объекты НЕ удаляются без gc.collect()
print("\n" + "="*50)
print("Правильно: Слабые ссылки")
print("="*50 + "\n")
node_c = Node('C')
node_d = Node('D')
# ПРАВИЛЬНО: Используем слабую ссылку
node_c.next = node_d
node_d.next = weakref.ref(node_c) # Слабая ссылка!
del node_c
del node_d
print("Объекты удалены сразу!")
Пример: Цикл в объекте (Parent-Child)
import weakref
import gc
class Parent:
def __init__(self, name):
self.name = name
self.children = []
def add_child(self, child):
self.children.append(child)
child.parent = self # Циклическая ссылка!
def __del__(self):
print(f"Parent {self.name} удалён")
class Child:
def __init__(self, name):
self.name = name
self.parent = None
def __del__(self):
print(f"Child {self.name} удалён")
print("Без слабых ссылок:")
parent1 = Parent('Mom')
child1 = Child('Alice')
parent1.add_child(child1)
del parent1, child1
print("Объекты не удалены!\n")
print("="*50)
print("Со слабыми ссылками:")
print("="*50 + "\n")
class BetterParent:
def __init__(self, name):
self.name = name
self.children = []
def add_child(self, child):
self.children.append(child)
child.parent = weakref.ref(self) # Слабая ссылка!
def __del__(self):
print(f"Parent {self.name} удалён")
parent2 = BetterParent('Dad')
child2 = Child('Bob')
parent2.add_child(child2)
del parent2, child2
print("Объекты удалены сразу!")
Отладка утечек памяти
import gc
import sys
# Получить объекты, которые не могут быть собраны
gc.collect()
garbages = gc.garbage
print(f"Несобираемые объекты: {len(garbages)}")
for obj in garbages[:5]: # Первые 5
print(f"Type: {type(obj)}, Size: {sys.getsizeof(obj)}")
# Найти все объекты определённого типа
from collections import defaultdict
object_counts = defaultdict(int)
for obj in gc.get_objects():
object_counts[type(obj).__name__] += 1
print("\nТоп-10 типов объектов:")
for obj_type, count in sorted(object_counts.items(), key=lambda x: x[1], reverse=True)[:10]:
print(f"{obj_type}: {count}")
Практические рекомендации
# 1. Используй слабые ссылки в parent-child отношениях
class Parent:
def add_child(self, child):
self.children.append(child)
child.parent = weakref.ref(self) # Слабая ссылка!
# 2. Используй контекстные менеджеры для очистки
class Resource:
def __enter__(self):
print("Ресурс захвачен")
return self
def __exit__(self, *args):
print("Ресурс освобожден")
# Автоматическая очистка циклических ссылок
with Resource():
pass # Гарантирует вызов __exit__
# 3. Явно удаляй циклические ссылки если нужно быстро
class Node:
def clean(self):
self.next = None # Удаляем цикл
node = Node()
node.clean() # Только потом удаляем
del node
Итоговая рекомендация
| Ситуация | Решение |
|---|---|
| Простые объекты | Полагайся на подсчёт ссылок (refcount) |
| Parent-child отношение | Используй weakref.ref() для parent |
| Linked list с циклами | Используй weakref.ref() для обратной ссылки |
| Обнаружение утечек | Используй gc.get_objects() и sys.getsizeof() |
| Production мониторинг | Отслеживай gc.get_stats() |
Главный совет: Python хорошо обрабатывает циклические ссылки через сборщик мусора, но лучше избегать их с помощью слабых ссылок. Это делает код более предсказуемым и экономит ресурсы сборщика мусора.