Как в Python работает сборка мусора при циклических ссылках

Ответ

Python использует комбинацию двух механизмов для управления памятью: подсчёт ссылок (reference counting) и циклический сборщик мусора (generational garbage collector).

  1. Подсчёт ссылок: Основной и самый быстрый механизм. У каждого объекта есть счётчик. Когда на объект создаётся новая ссылка, счётчик увеличивается. Когда ссылка удаляется, счётчик уменьшается. Как только счётчик достигает нуля, объект немедленно удаляется.

  2. Проблема: Подсчёт ссылок не работает для циклических зависимостей. В этом случае счётчики ссылок объектов в цикле никогда не достигнут нуля, даже если на них нет внешних ссылок. Это приводит к утечке памяти.

    # Создание циклической ссылки
    a = []
    b = []
    a.append(b)
    b.append(a)
    
    # Удаляем внешние ссылки на a и b
    del a
    del b
    
    # Объекты-списки все еще ссылаются друг на друга,
    # их счетчики ссылок равны 1. Память не освобождается.
  3. Решение: Для обнаружения и очистки таких циклов в Python есть generational garbage collector. Он периодически запускается и ищет группы объектов, на которые можно сослаться только изнутри этих же групп. Найдя такой "остров" недостижимых объектов, он их удаляет.

Для ручного управления можно использовать модуль gc или слабые ссылки weakref:

  • Принудительный запуск сборщика: import gc; gc.collect()
  • Слабые ссылки: Модуль weakref позволяет создавать ссылки на объекты, которые не увеличивают их счётчик ссылок, тем самым предотвращая образование циклов.

Ответ 18+ 🔞

А, слушай, про память в Python — это вообще отдельная песня, блядь! Ну, представь себе: язык вроде умный, а сам по себе, как мартышлюшка, за собой убрать не всегда может. Так вот, чтобы не захлебнуться в собственном мусоре, он использует две хитрые, блядь, штуки.

Первая — это подсчёт ссылок, ну, самая простая и быстрая. У каждого объекта, будь то список, строка или твоя собственная ебанутая класс-заглушка, есть счётчик. Как только ты на него начинаешь показывать пальцем — счётчик плюс один. Перестал показывать — минус один. Как только доходит до нуля — объект тут же, нахуй, отправляется в небытие. Всё чётко, быстро, без лишних вопросов.

Но, ёпта, есть же проблема! Эта система, как хитрая жопа, не видит циклических зависимостей. Вот смотри, какой пиздец можно устроить:

# Создаём два пустых списка
a = []
b = []
# А теперь делаем так, чтобы они любили друг друга до гроба
a.append(b)  # 'a' теперь содержит ссылку на 'b'
b.append(a)  # 'b' теперь содержит ссылку на 'a'

# И вот мы, такие умные, удаляем внешние ссылки
del a
del b

# А объекты-то живы! Они друг на друга смотрят, счётчики у них по единичке.
# И сидят они там, в памяти, как два придурка, и ждут конца света.
# Память не освобождается. Вот тебе и утечка, блядь!

Ну и что делать, спросишь? А вот для этого есть второй механизм, generational garbage collector, или циклический сборщик мусора. Этот товарищ, как заправский детектив, периодически прочёсывает память и ищет вот такие "острова" из объектов, которые друг на друга ссылаются, но до них больше никто не может дотянуться. Нашёл — и хуяк, на свалку истории! Освободил память, герой, блядь.

А если ты совсем, сука, параноик и хочешь всё контролировать руками, то есть модули gc и weakref.

  • Хочешь принудительно запустить уборку? import gc; gc.collect() — и вперёд.
  • Не хочешь плодить эти ебучіе циклические ссылки? Используй слабые ссылки через weakref. Это такие ссылки, которые, как сосед-алкаш, на тебя смотрят, но в твои дела не лезут и счётчик не увеличивают. Удобно, ёпта!

Вот так вот, не так всё просто, как кажется. А ты думал, Python — это просто скрипты писать?