
В принципе Python спроектирован так, чтобы почти не заботиться об управлении памятью. Однако знание того, как все устроено, помогает писать более качественный код и избегать всяческих экзотических фиаско при выполнении вашего кода… и помогает проходить успешно собеседования.
Здесь я изложу основные тезисы об управлении памятью в Python (CPython).
• В Python память управляется автоматически.
• Память для объектов, которые уже не нужны освобождается сборщиком мусора.
• Для небольших объектов (< 512 байт) Python выделяет и освобождает память блоками (в блоке может быть несколько объектов). Почему: операции с блоками памятью через ОС довольно долгие, а мелких объектов обычно много, и, таким образом, системные вызовы совершаются не так часто.
• Есть два алгоритма сборки мусора: подсчет ссылок (reference counting) и сборщик на основе поколений (generational garbage collector — gc).
• Алгоритм подсчета ссылок очень простой и эффективный, но у него есть один большой недостаток (помимо многих мелких). Он не умеет определять циклические ссылки
• Циклическими ссылками занимается gc, о ним чуть позже.
• Переменные хранят ссылки на объекты в памяти, внутри объект хранит числовое поле – количество ссылок на него (несколько переменных могут ссылаться на один объект)
• Количество ссылок увеличивается при присвоении, передаче аргументов в функцию, вставке объекта в список и т.п.
• Если число ссылок достигло 0, то объект сразу удаляется (это плюс).
• Если при удалении объект содержал ссылки на другие объекты, то и те могут удалиться, если это были последние ссылки.
• Переменные, объявленные вне функций, классов, блоков – глобальные.
• Глобальные переменные живут до конца процесса Python, счетчик их ссылок никогда не падает до нуля.
• При выходе из блока кода, ссылки созданные локальными переменными области видимости этого блока – уничтожаются.
• Функция sys.getrefcount позволит узнать число ссылок на объект (правда она накинет единицу, т.к. ее аргумент — тоже ссылка на тестируемый объект):
>>> foo = [] >>> import sys >>> sys.getrefcount(foo) 2 >>> def bar(a): print(sys.getrefcount(a)) ... >>> bar(foo) 4 >>> sys.getrefcount(foo) 2
• Подсчет ссылок в CPython — исторически. Вокруг него много дебатов. В частности наличие GIL многим обязано этому алгоритму.
• Пример создания циклической ссылки – добавим список в себя:
lst = [] lst.append(lst)
• Цикличные ссылки обычно возникают в задачах на графы или структуры данных с отношениями между собой.
• Цикличные ссылки могут происходить только в “контейнерных” объектах (списки, словари, …).
• GC запускается переодически по особым условиям; запуск GC создает микропаузы в работе кода.
• GC разделяет все объекты на 3 поколения. Новые объекты попадают в первое поколение.
• Как правило, большинство объектов живет недолго (пример: локальные переменные в функции). Поэтому сборка мусора в первом поколении выполняется чаще.
• Если новый объект выживает процесс сборки мусора, то он перемещается в следующее поколение. Чем выше поколение, тем реже оно сканируется на мусор.
• Во время сборки мусора объекты поколения, где он собирается, сканируются на наличие циклических ссылок; если никаких ссылок, кроме циклических нет — то объекты удаляются.
• Можно использовать инструменты из модуля weakref для создания слабых ссылок.
• Слабые ссылки не учитываются при подсчете ссылок. Если объект, на который ссылается слабая ссылка, удалится, то слабая ссылка просто обнулится, станет пустышкой.
• Подсчет ссылок не может быть отключен, а gc — может.
• В некоторых случаях полезно отключить автоматическую сборку gc.disable()
и вызывать его вручную gc.collect()
.
Специально для канала @pyway.