Как Python высвобождает память Python

Как Python высвобождает память

Python является высокоуровневым языком программирования. Это означает, что вам нечасто придется думать о том, как именно хранятся объекты в памяти. Однако если вы задумываетесь о производительности ваших программ, стоит получше разобраться с тем, как Python обращается с лишними объектами.

Как Python работает с памятью

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

У этой проблемы есть несколько решений. Можно быть аккуратным, и не захламлять стол — уничтожать объекты после использования. Но что, если завести помощника, который будет делать это за вас? В таком случае, не придется отвлекаться на уборку, но есть риск, что помощник выкинет что-то нужное, или оставит что-то лишнее.

Оба этих подхода реализованы в Python. Уничтожать объекты вручную можно с помощью функции del(), однако все не так просто, как может показаться. Переменные в Python всего лишь ссылаются на объекты, записанные в ячейки памяти. На один и тот же объект может быть множество ссылок. Для небольших, часто используемых объектов было бы расточительно освобождать ячейку памяти. Поэтому функция del() всего лишь удаляет ссылку. Проверим этот факт с помощью функции id(), возвращающей адрес объекта в памяти.

>>> a = 0
>>> id(a)
2349134932240
... del(a)
... id(0)
2349134932240

За сохранение простых и часто используемых объектов от уничтожения отвечает менеджер памяти. Он позволяет программам на Python работать эффективнее. Но большие, редко используемые объекты при этом действительно удаляются.

>>> a = 1234567890
>>> id(a)
2349186213648
>>> del(a)
>>> id(1234567890)
2349186215792

На самом деле, за полноценное удаление объектов с высвобождением памяти отвечает не функция del(), а специальный алгоритм.

Подсчет ссылок

Первые кандидаты на удаление — объекты, на которые ничто не ссылается. Каждый объект имеет дополнительное поле, в котором записано текущее количество ссылок на него. Количество этих ссылок можно посмотреть, воспользовавшись библиотекой os:

>>> print(sys.getrefcount('rare object'))
2
>>> print(sys.getrefcount(0))
1920

Очевидно, функция sys.getrefcount() сама ссылается на объекты — не удивляйтесь, что на свежесозданный объект ведет сразу две ссылки. На небольшие числа часто ссылается сам интерпретатор питона, поэтому количество ссылок на них будет довольно большим.

Если число ссылок на объект дойдет до нуля, то он будет уничтожен, а ячейка памяти освободится для перезаписи. Количество ссылок у всех объектов, на которые ссылался удаленный уменьшится на один. Если мы присвоим переменной соответствующее значение, то количество ссылок наоборот увеличится.

Этот простой алгоритм делает большую часть работы по сбору мусора. При выходе из блока кода (который фактически является локальной областью видимости), он уничтожит все ссылки, созданные локальными переменными. Подсчет ссылок удаляет лишние объекты моментально, что позволяет сделать работу программы предсказуемой. Его нельзя отключить — это базовый алгоритм, который обеспечивает нормальную работу кода.

Подсчет ссылок — исторически первый алгоритм сбора мусора в Python, и имеет свои недостатки. Он отлично работает с неизменяемыми объектами, которые ссылаются только на одну ячейку памяти (строки, числа...) или на несколько неизменяемых объектов (простые кортежи, замороженные множества...). Но объекты-контейнеры могут обмануть этот механизм, ссылаясь сами на себя или друг на друга.

# Объект ссылается сам на себя
lst = []
lst.append(lst)
# Объекты ссылаются друг на друга
lst1 = []
lst2 = []
lst1.append(lst2)
lst2.append(lst1)

В таком случае, количество ссылок на эти объекты никогда не снизится до нуля. Именно для этого в Python существует дополнительный сборщик мусора.

Generational garbage collector

В отличие от подсчета ссылок, сборщик мусора (gc) не работает постоянно, а изучает разные объекты с разной частотой. Этот процесс работает по тому же принципу, что и наша память. Объекты разделяются на три генерации по сроку своей жизни. В первое поколение попадают новые объекты, большинство из которых не доживет даже до первой сборки мусора. Если какой-то объект не уничтожается, он переходит в следующее поколение.

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

>>> import gc
... gc.get_threshold()
(700, 10, 10)
>>> gc.set_treshold(500, 10, 10)

Сам процесс поиска циклических ссылок достаточно прост. Сначала создается множество, куда заносятся все созданные объекты-контейнеры. Каждый из них получает по счетчику, значение которого будет равно количеству ссылок на объект. Алгоритм проходится по каждому объекту множества. Каждый контейнер, на который он ссылался, уменьшает новый счетчик на единицу.

Контейнер Счетчик Ссылки
lst1 ~~1~~ 0 lst
lst2 ~~1~~ 0 lst3
lst3 ~~2~~ 1 lst2, outer
lst4 2 outer, outer

Все контейнеры, у которых осталось больше нуля ссылок, исключаются из множества (В нашем примере - lst3 и lst4). На них ссылаются другие объекты, не вошедшие в список. Все контейнеры, на которые ссылаются исключенные, также исключаются (lst2). Все, что осталось во множестве (lst1) может быть удалено. На эти объекты ссылаются лишь они сами или другие контейнеры, на которые больше ничто не ссылается.

Модуль gc позволяет отключать этот алгоритм, запускать его вручную и настраивать. Однако на практике в этом редко возникает необходимость.

Заключение

Переменные в Python хранят не объекты, а ссылки на них. Благодаря подсчету ссылок, интерпретатор избавляется от ненужных объектов. От объектов, имеющих циклические ссылки избавляется дополнительный сборщик мусора. Эти два механизма позволяют нашим программам экономить оперативную память устройства.

Практический Python для начинающих
Практический Python для начинающих

Станьте junior Python программистом за 7 месяцев

 7 месяцев

Возможно будет интересно

🏆 Hello, world! Python
Новичок
🏆 Hello, world!

Мы вчера запустили новый www.pylot.me. Должны были в следующую среду, но запустили вчера.

2022-10-04
Как практиковаться в Python? Python
Новичок
Как практиковаться в Python?

Для улучшения качества знаний и повышения уровня программиста, необходим постоянный практикум. Где можно это организовать самостоятельно, и как практиковаться в Python?

2022-10-19
Условные конструкции и сопоставление структурных шаблонов Шпаргалки
Новичок
Условные конструкции и сопоставление структурных шаблонов

Шпаргалка по условным конструкциям и сопоставлению структурных шаблонов

2022-11-09