Объясните основные механизмы работы сборщика мусора (GC) в Python

Ответ

В CPython управление памятью автоматизировано с помощью двух основных механизмов: подсчёта ссылок и циклического сборщика мусора.

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

Это основной и самый быстрый механизм. Каждый объект в Python имеет внутренний счётчик, который отслеживает количество ссылок на него.

  • Когда на объект создается новая ссылка (например, присваивание переменной), его счётчик увеличивается.
  • Когда ссылка удаляется (del, выход из области видимости), счётчик уменьшается.
  • Как только счётчик достигает нуля, объект немедленно удаляется из памяти.
import sys

my_list = [1, 2, 3]  # Создан объект, счетчик ссылок = 1
print(sys.getrefcount(my_list) - 1) # Вывод: 1

another_list = my_list # Новая ссылка, счетчик = 2
print(sys.getrefcount(my_list) - 1) # Вывод: 2

del another_list # Ссылка удалена, счетчик = 1
print(sys.getrefcount(my_list) - 1) # Вывод: 1

del my_list # Последняя ссылка удалена, счетчик = 0, объект удаляется

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

2. Циклический сборщик (Generational GC)

Для решения проблемы циклических ссылок Python использует дополнительный алгоритм — сборщик мусора по поколениям.

  • Принцип: Он основан на гипотезе, что большинство объектов "умирают молодыми". Поэтому все объекты разделены на три поколения (0, 1, 2).
  • Процесс:
    1. Новые объекты попадают в поколение 0.
    2. Сборщик запускается чаще всего для самого молодого поколения.
    3. Он находит "недостижимые" объекты (включая циклические ссылки) с помощью алгоритма mark-and-sweep (пометить и убрать).
    4. Объекты, которые "выжили" после нескольких сборок в младшем поколении, перемещаются в старшее (из 0 в 1, из 1 в 2).

Управлять сборщиком можно с помощью модуля gc:

import gc

# Проверить, включен ли GC
print(gc.isenabled())

# Принудительно запустить сборку мусора
gc.collect()

Ответ 18+ 🔞

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

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

Это главный пахан, быстрый, как хуй с горы. У каждого объекта в Python есть такой внутренний счётчик, который считает, сколько на него ссылок смотрит.

  • Создал новую ссылку — счётчик плюсанул.
  • Удалил ссылку (del или просто переменная за scope вышла) — счётчик минусанул.
  • Как только счётчик в ноль ушёл — объект тут же, нахуй, в небытие отправляется. Мгновенно!
import sys

my_list = [1, 2, 3]  # Создали объект, счётчик = 1
print(sys.getrefcount(my_list) - 1) # Вывод: 1

another_list = my_list # Новая ссылка, счётчик = 2
print(sys.getrefcount(my_list) - 1) # Вывод: 2

del another_list # Ссылку убили, счётчик = 1
print(sys.getrefcount(my_list) - 1) # Вывод: 1

del my_list # Последнюю ссылку прикончили, счётчик = 0, объект — пиздец.

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

2. Циклический сборщик (Generational GC)

Вот для таких хитрожопых случаев есть второй мужик — сборщик мусора по поколениям. Он типа подчищает за первым.

  • Идея простая: большинство объектов дохнут молодыми. Поэтому их всех поделили на три поколения (0, 1, 2).
  • Как работает:
    1. Новый объект — сразу в поколение 0, на передовую.
    2. Сборщик чаще всего молодняк и проверяет, как салага.
    3. Ищет там недостижимое говно (включая эти ебучие циклические связи) по алгоритму «пометил и выкинул».
    4. Кто выжил после нескольких чисток в молодом поколении — того переводят в старшее (из 0 в 1, из 1 в 2). Как повышение, блядь.

Поковыряться в нём можно через модуль gc:

import gc

# Проверить, не выключили ли его случайно
print(gc.isenabled())

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

Вот так вот, в двух словах. Один работает быстро, но тупит на циклах, второй — медленнее, но подчищает за первым. Вместе — овердохуища работы делают, чтобы память не текла, как из дырявого ведра.