← Назад к вопросам
Расскажи про две версии сборщика мусора (Garbage collector) в Python
1.8 Middle🔥 161 комментариев
#Python Core#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Сборщик мусора в Python: две версии и эволюция
Сборщик мусора (Garbage Collector) в Python прошёл значительную эволюцию. Разберём основные версии, их работу и различия.
Reference Counting (Подсчёт ссылок)
Это основной механизм управления памятью в Python, который работает всегда:
import sys
# Создание объекта
x = []
print(sys.getrefcount(x)) # 2 (сама ссылка + параметр getrefcount)
y = x # Добавляем ещё одну ссылку
print(sys.getrefcount(x)) # 3
del y # Удаляем ссылку
print(sys.getrefcount(x)) # 2
# Когда счётчик ссылок = 0, объект удаляется немедленно
def create_list():
temp = [1, 2, 3] # счётчик = 1
# Когда функция заканчивается, счётчик = 0
# Объект удаляется из памяти СРАЗУ
create_list()
Плюсы reference counting:
- Детерминированное удаление (освобождение памяти происходит сразу)
- Предсказуемое поведение
- Низкие overhead для простых случаев
- Объекты удаляются в правильное время (финализаторы вызываются сразу)
class Resource:
def __init__(self, name):
self.name = name
def __del__(self):
print(f"Resource {self.name} is being freed")
r = Resource("db_connection")
print("Before delete")
del r # Выведет "Resource db_connection is being freed" СРАЗУ
print("After delete")
Минусы reference counting:
- Не справляется с циклическими ссылками
- Overhead на каждое изменение ссылки
Циклические ссылки - проблема reference counting
import sys
# Проблема: циклическая ссылка
class Node:
def __init__(self, value):
self.value = value
self.next = None
node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1 # Циклическая ссылка!
print(f"node1 refcount: {sys.getrefcount(node1)}")
print(f"node2 refcount: {sys.getrefcount(node2)}")
del node1 # Счётчик не = 0 из-за циклической ссылки!
del node2 # Объекты остаются в памяти!
# УТЕЧКА ПАМЯТИ
Mark-and-Sweep GC (Пометка и метание)
Периодический сборщик мусора, работающий отдельно:
import gc
# Получение информации о сборщике мусора
print(gc.get_count()) # (700, 10, 10) - счётчики для трёх поколений
# Объекты с циклическими ссылками
class Node:
def __init__(self, value):
self.value = value
self.next = None
node1 = Node(1)
node2 = Node(2)
node1.next = node2
node2.next = node1
del node1
del node2
# Объекты ещё в памяти, но mark-and-sweep найдёт их
gc.collect() # Явно запускаем сборщик
print("Garbage collected")
# Отключение автоматического сборщика
gc.disable()
# Работаем без сборщика мусора
for i in range(1000000):
x = list(range(1000))
# Собираем мусор явно
gc.collect()
gc.enable()
Поколения (Generational GC)
Mark-and-Sweep работает поколениями для оптимизации:
import gc
# Поколение 0: молодые объекты (сбор часто)
# Поколение 1: средние объекты (сбор редче)
# Поколение 2: старые объекты (сбор ещё реже)
print(gc.get_threshold()) # (700, 10, 10)
# 700: сборка поколения 0 после создания 700 объектов
# 10: сборка поколения 1 после 10 сборок поколения 0
# 10: сборка поколения 2 после 10 сборок поколения 1
# Изменение параметров
gc.set_threshold(1000, 15, 15)
# Явная сборка конкретного поколения
gc.collect(generation=0) # Собрать только молодые объекты
gc.collect(generation=1) # Собрать среднего возраста
gc.collect(generation=2) # Собрать все (самое долгое)
Эволюция: Python 3.11+ с Per-Interpreter GIL
Novella версия даёт больше контроля:
# Python 3.11+ - больше контроля над GC
import gc
# Включение/отключение сборщика
if gc.isenabled():
print("GC enabled")
else:
print("GC disabled")
# Объекты, отслеживаемые сборщиком
print(gc.get_tracked_objects()) # Объекты с циклическими ссылками
# Отладка утечек памяти
gc.set_debug(gc.DEBUG_SAVEALL) # Сохранять удаляемые объекты
# После сборки мусора
gc.collect()
# Получение объектов, которые были удалены
unreachable = gc.garbage
for obj in unreachable:
print(f"Unreachable: {type(obj).__name__}")
gc.set_debug(0) # Отключить режим отладки
Практические примеры
Пример 1: Обнаружение циклических ссылок
import gc
class LinkedList:
def __init__(self, value):
self.value = value
self.next = None
# Создание циклической ссылки
head = LinkedList(1)
current = head
for i in range(2, 5):
current.next = LinkedList(i)
current = current.next
# Замыкаем цикл
current.next = head
# Удаляем все ссылки
del head
del current
# Reference counting найдёт только часть
# Остальное найдёт GC при сборке
gc.collect()
print("Cyclic references cleaned up")
Пример 2: Оптимизация production кода
import gc
import time
def process_large_dataset():
# Отключаем автоматический GC для быстрой обработки
gc_was_enabled = gc.isenabled()
gc.disable()
try:
# Интенсивная обработка
results = []
for i in range(1000000):
data = process_item(i)
if data:
results.append(data)
# Периодически собираем мусор вручную
if i % 10000 == 0:
gc.collect(generation=0) # Собрать только молодые
return results
finally:
# Восстанавливаем исходное состояние
if gc_was_enabled:
gc.enable()
gc.collect() # Финальная очистка
def process_item(i):
return i * 2 if i % 2 == 0 else None
Пример 3: Отследение утечек памяти
import gc
import sys
from typing import List
class MemoryLeakDetector:
@staticmethod
def find_cycles():
"""Найти циклические ссылки"""
gc.set_debug(gc.DEBUG_SAVEALL)
before = len(gc.get_objects())
gc.collect()
after = len(gc.get_objects())
print(f"Объектов до сборки: {before}")
print(f"Объектов после сборки: {after}")
print(f"Удалено: {before - after}")
# Анализ типов удаляемых объектов
if gc.garbage:
print("\nУдаляемые объекты:")
type_counts = {}
for obj in gc.garbage:
obj_type = type(obj).__name__
type_counts[obj_type] = type_counts.get(obj_type, 0) + 1
for obj_type, count in sorted(type_counts.items(), key=lambda x: x[1], reverse=True):
print(f" {obj_type}: {count}")
gc.set_debug(0)
gc.garbage.clear()
@staticmethod
def get_memory_info():
"""Информация об использовании памяти"""
stats = gc.get_stats()
for gen, stat in enumerate(stats):
print(f"Generation {gen}:")
print(f" Collections: {stat.get('collections', 0)}")
print(f" Collected: {stat.get('collected', 0)}")
print(f" Uncollectable: {stat.get('uncollectable', 0)}")
detector = MemoryLeakDetector()
detector.find_cycles()
detector.get_memory_info()
Сравнение двух подходов
| Характеристика | Reference Counting | Mark-and-Sweep GC |
|---|---|---|
| Когда работает | Постоянно | Периодически |
| Скорость обнаружения утечек | Моментально (если нет циклов) | Отложенная |
| Циклические ссылки | Не обрабатывает | Обрабатывает |
| Overhead памяти | На каждый объект | Периодическая сборка |
| Предсказуемость | Высокая | Низкая (может резко замедлить) |
| GIL | Необходим для потокобезопасности | Может быть оптимизирован |
Best Practices
# 1. Избегай циклических ссылок
class BadDesign:
def __init__(self):
self.circular = self # Плохо!
# 2. Используй слабые ссылки для обратных ссылок
import weakref
class Parent:
def __init__(self):
self.children = []
class Child:
def __init__(self, parent):
self.parent = weakref.ref(parent) # Слабая ссылка
# 3. Явно удаляй большие объекты
big_list = [0] * 1000000
data = process(big_list)
del big_list # Освобождаем память сразу
# 4. Мониторь использование памяти в production
import tracemalloc
tracemalloc.start()
# ... код ...
current, peak = tracemalloc.get_traced_memory()
print(f"Current: {current / 1024 / 1024:.2f} MB")
print(f"Peak: {peak / 1024 / 1024:.2f} MB")
tracemalloc.stop()
В Python используются оба механизма: Reference Counting как основной для простых случаев, и Mark-and-Sweep как дополнение для циклических ссылок и оптимизации. Это гибридный подход, обеспечивающий хорошую производительность и надежность.