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

Как сборщик мусора работает с циклическими ссылками?

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 хорошо обрабатывает циклические ссылки через сборщик мусора, но лучше избегать их с помощью слабых ссылок. Это делает код более предсказуемым и экономит ресурсы сборщика мусора.