Память: подсчёт ссылок и сборщик мусора
Объекты и ссылки
Каждое значение в Python — объект в куче. Переменные — имена, ссылающиеся на объекты. У каждого объекта CPython есть счётчик ссылок (ob_refcnt): сколько имён, элементов контейнеров и атрибутов на него указывает.
import sys
data = [1, 2, 3]
print(sys.getrefcount(data)) # +1: сам аргумент getrefcount — тоже временная ссылка
alias = data
container = [data, data]
print(sys.getrefcount(data)) # data, alias, 2 элемента списка, аргумент
del alias
container.clear()
print(sys.getrefcount(data))
Пройдите пример 👣 Пошагово: визуализатор покажет, как на один объект-список указывают несколько имён.
Подсчёт ссылок
Когда счётчик падает до нуля, объект освобождается немедленно — детерминированно, в тот же момент:
class Resource:
def __init__(self, name):
self.name = name
print(f" создан {name}")
def __del__(self): # вызывается при уничтожении объекта
print(f" уничтожен {self.name}")
def work():
r = Resource("временный")
print(" работаю")
# выход из функции: ссылок нет → __del__
work()
print("после work()")
a = Resource("A")
b = a
del a
print("del a — объект жив: на него смотрит b")
b = None
print("b = None — последняя ссылка исчезла")
Плюсы подсчёта ссылок: память освобождается сразу, нет долгих пауз. Минусы: накладные расходы на каждую операцию с ссылками (одна из причин GIL) и циклы.
Циклические ссылки
import gc
class Node:
def __init__(self, name):
self.name = name
self.partner = None
def __del__(self):
print(f" уничтожен {self.name}")
gc.disable() # выключим сборщик, чтобы увидеть проблему
a, b = Node("A"), Node("B")
a.partner, b.partner = b, a # цикл: A → B → A
del a, b # имён нет, но счётчики = 1 (ссылаются друг на друга)
print("после del — объекты всё ещё в памяти")
print("собрано сборщиком:", gc.collect()) # сборщик находит недостижимый цикл
gc.enable()
Сборщик мусора (gc)
Модуль gc — дополнительный механизм, который находит недостижимые циклы среди объектов-контейнеров (списки, словари, экземпляры классов).
Он поколенческий: большинство объектов живут недолго, поэтому новые объекты проверяются чаще.
| Поколение | Кто там | Как часто проверяется |
|---|---|---|
| 0 | новые объекты | когда создано больше выделений, чем порог (по умолчанию 700/2000) |
| 1 | пережившие одну сборку | реже |
| 2 | долгожители | редко |
import gc
print("пороги:", gc.get_threshold())
print("объектов по поколениям:", gc.get_count())
print("включён:", gc.isenabled())
print("отслеживается ли int:", gc.is_tracked(42), "| список:", gc.is_tracked([]))
Неизменяемые атомы (числа, строки) сборщик не отслеживает — они не могут образовать цикл.
gc в продакшене
Сборщик можно отключить (gc.disable()) или «заморозить» объекты после старта (gc.freeze()) — так делал Instagram, чтобы уменьшить копирование памяти в форкнутых воркерах. Но обычно его не трогают.
del — ненадёжный финализатор
- момент вызова не гарантирован (при циклах — когда придёт сборщик; в PyPy — без подсчёта ссылок вообще);
- при завершении интерпретатора может не вызваться;
- исключения в нём игнорируются (печатается предупреждение).
Для освобождения ресурсов используйте with и контекстные менеджеры, а для «напоминания» — weakref.finalize.
Слабые ссылки
Слабая ссылка указывает на объект, но не увеличивает счётчик — не мешает его удалению:
import weakref, gc
class BigImage:
def __init__(self, name):
self.name = name
img = BigImage("фото.jpg")
ref = weakref.ref(img)
print(ref(), ref().name) # обратиться к объекту — вызвать ссылку
del img
gc.collect()
print(ref()) # объект удалён → None
cache = weakref.WeakValueDictionary()
a = BigImage("a.png")
cache["a"] = a
print(list(cache.keys()))
del a
gc.collect()
print(list(cache.keys())) # запись исчезла сама
obj = BigImage("с финализатором")
weakref.finalize(obj, print, " finalize: освобождаю ресурс")
del obj
Применение: кеши, которые не удерживают объекты; связи «дочерний → родитель» (чтобы избежать цикла); реестры наблюдателей.
Аллокатор и «процесс не отдаёт память»
CPython выделяет маленькие объекты (≤ 512 байт) своим аллокатором pymalloc: блоки по размерам, сгруппированные в «пулы» по 4 КБ и «арены» по 256 КБ (в 3.10+ — 1 МБ). Арена возвращается ОС, только если в ней не осталось ни одного живого объекта.
Поэтому после удаления большого списка потребление памяти процессом (RSS) может не уменьшиться — память переиспользуется внутри Python для новых объектов. Это не утечка.
import sys
print(sys.getsizeof(0), sys.getsizeof(10**100), sys.getsizeof(""), sys.getsizeof("abc"))
print(sys.getsizeof([]), sys.getsizeof([1, 2, 3]), sys.getsizeof({}), sys.getsizeof(set()))
nums = [1, 2, 3]
print("getsizeof не считает вложенные объекты:", sys.getsizeof(nums), "байт только сам список")
Утечки памяти и их поиск
В Python «утечка» — это почти всегда объекты, на которые кто-то ещё ссылается:
- неограниченные кеши и глобальные списки (
results.append(...)в модуле); lru_cacheбезmaxsizeили на методах (держитself);- подписки на события без отписки;
- замыкания и трассировки исключений, удерживающие большие объекты;
- циклы с
__del__в старых версиях Python.
import tracemalloc
tracemalloc.start()
leak = []
for i in range(10_000):
leak.append("x" * 100 + str(i)) # «утечка»: растущий глобальный список
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics("lineno")[:2]:
print(stat)
current, peak = tracemalloc.get_traced_memory()
print(f"сейчас {current / 1024:.0f} КБ, пик {peak / 1024:.0f} КБ")
tracemalloc.stop()
Инструменты: tracemalloc (stdlib, сравнение снимков), objgraph (кто держит объект), memray и py-spy (профилирование памяти в продакшене).
Вопрос на собеседовании
«Как в Python управляется память?» — Основной механизм — подсчёт ссылок: объект освобождается, как только счётчик становится нулём. Для циклических ссылок есть поколенческий сборщик мусора (gc), который периодически находит недостижимые группы объектов. Маленькие объекты выделяются аллокатором pymalloc. Слабые ссылки позволяют ссылаться на объект, не удерживая его.
Итог
- Каждый объект хранит счётчик ссылок; при нуле — немедленное освобождение.
delудаляет имя, а не объект.- Циклы освобождает поколенческий сборщик
gc. __del__ненадёжен — ресурсы освобождаютwith;weakref— ссылки без удержания.- Утечки — это забытые ссылки; искать с
tracemalloc.
Практика решено 0 из 5
Сделайте кеш, который не удерживает объекты в памяти: класс ImageCache хранит изображения
в weakref.WeakValueDictionary. Методы: put(key, image) и get(key) (вернуть объект или None).
Когда на изображение не остаётся других ссылок, оно должно исчезнуть из кеша само.
Какие тесты проверяют код (1)
import gc
class Image:
pass
cache = ImageCache()
img = Image()
cache.put("logo", img)
assert cache.get("logo") is img
del img
gc.collect()
assert cache.get("logo") is None, "Кеш не должен удерживать объект, на который больше нет ссылок"
import weakref
class ImageCache:
def __init__(self):
self._data = weakref.WeakValueDictionary()
def put(self, key, image):
self._data[key] = image
def get(self, key):
return self._data.get(key)
Найдите утечку: функция make_handlers(n) создаёт объекты Node, которые ссылаются друг на друга
по кругу (a.next = b, b.next = a). Напишите leaked_after_del(n): создать n таких пар,
удалить все ссылки на них, затем вернуть, сколько объектов собрал gc.collect()
(это число > 0 — циклы освобождает только сборщик мусора, а не подсчёт ссылок).
Перед созданием пар вызовите gc.collect(), чтобы не учитывать чужой мусор.
Какие тесты проверяют код (2)
assert leaked_after_del(100) >= 200, "Ожидалось, что сборщик соберёт как минимум 200 объектов Node" assert leaked_after_del(10) >= 20
import gc
class Node:
def __init__(self):
self.next = None
def leaked_after_del(n):
gc.collect()
pairs = []
for _ in range(n):
a, b = Node(), Node()
a.next, b.next = b, a
pairs.append(a)
del pairs, a, b
return gc.collect()
Когда CPython освобождает объект, на который больше нет ссылок и который не участвует в циклах?
Что делает del x?
Что может приводить к росту памяти («утечке») в долгоживущем Python-сервисе?
Войдите, чтобы отмечать пройденные уроки.