Сборка мусора

Материал из llmdocs — справочник разработчика
Портал: Основы CS
Сборка мусора
ПоявлениеLisp, Джон Маккарти (1959)
Подходыподсчёт ссылок, трассировка (mark-sweep, mark-compact, копирование)
Улучшенияпоколения, инкрементальная и конкурентная сборка
Языки с GCJava, C#, Go, Python, JavaScript, Ruby, Haskell
Без GCC, C++ (RAII), Rust (владение)
Метрикипаузы, пропускная способность, расход памяти

Сборка мусора (англ. garbage collection, GC) — автоматическое управление памятью, при котором среда выполнения сама находит объекты, недоступные программе, и освобождает занятую ими память. Программисту не нужно вызывать free или delete, а значит исчезают целые классы ошибок: утечки из-за забытого освобождения, повторное освобождение и обращение к уже освобождённой памяти (use-after-free). Платой становятся дополнительный расход процессора и памяти и паузы в работе программы. Сравнение с ручным управлением и моделью владения Rust — в статье Сборка мусора vs ручное управление памятью.

История

Сборку мусора изобрёл Джон Маккарти в 1959 году для языка Lisp: список, ставший ненужным, должен был возвращаться в свободную память автоматически.[2] Долгое время GC считался роскошью для академических языков, но с выходом Java (1995) стал нормой в промышленной разработке. Сегодня сборщик мусора есть в Java, C#, Go, Python, JavaScript, Ruby, Kotlin, Swift (через подсчёт ссылок) и большинстве скриптовых языков.

Что считается мусором

Объект «жив», если до него можно добраться по ссылкам от корней — глобальных переменных, локальных переменных в стеках потоков, регистров. Всё остальное — мусор, даже если на него ещё ссылаются другие недоступные объекты. Сборщик не знает, понадобится ли объект в будущем, — он консервативно сохраняет всё достижимое. Поэтому утечки памяти возможны и с GC: например, когда объекты бесконечно накапливаются в глобальном кэше или списке подписчиков.

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

У каждого объекта есть счётчик ссылок на него; когда счётчик падает до нуля, объект немедленно освобождается.

  • Плюсы: память освобождается сразу и предсказуемо, нет длинных пауз.
  • Минусы: накладные расходы на каждое присваивание (особенно атомарные операции в многопоточной среде) и неспособность собрать циклы — два объекта, ссылающиеся друг на друга, никогда не получат нулевой счётчик.

Так работают CPython (с дополнительным сборщиком циклов, модуль gc),[4] Swift и Objective-C (ARC), shared_ptr в C++ и Rc/Arc в Rust. В Swift и Rust циклы разрывают слабыми ссылками (weak).

Трассирующие сборщики

Трассирующий сборщик периодически обходит граф объектов от корней:[1]

Алгоритм Как работает Особенности
Mark-sweep пометить достижимые, затем пройти по куче и освободить непомеченные память фрагментируется
Mark-compact после пометки сдвинуть живые объекты подряд нет фрагментации, но дорогое перемещение
Копирующий копировать живые объекты в другую половину памяти быстрое выделение, время зависит только от живых объектов; нужна вдвое большая память

Поколения

Эмпирическая гипотеза поколений: большинство объектов умирает молодыми (временные строки, промежуточные результаты). Поэтому кучу делят на молодое и старое поколения. Молодое собирается часто и быстро копирующим сборщиком, а пережившие несколько сборок объекты переводятся в старое, которое собирается редко. На этом построены сборщики JVM, .NET и V8.[6]

Паузы

Простой сборщик останавливает программу на время работы (stop-the-world). Чтобы сократить паузы, применяют:

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

Сборщики в популярных языках

  • Java (HotSpot): G1 — по умолчанию, баланс пауз и пропускной способности; ZGC и Shenandoah — паузы в доли миллисекунды даже на кучах в терабайты; Parallel GC — максимальная пропускная способность для пакетных задач.[5]
  • Go: конкурентный трёхцветный mark-sweep без поколений и без перемещения объектов, оптимизирован на короткие паузы. Настраивается двумя параметрами: GOGC (насколько куча может вырасти между сборками, по умолчанию 100%) и GOMEMLIMIT (мягкий предел памяти).[3]
  • Python: подсчёт ссылок плюс поколенческий сборщик циклов.
  • JavaScript (V8): поколенческий сборщик Orinoco с параллельной и конкурентной работой.

Практика

  • Создавайте меньше временных объектов в горячих участках кода: переиспользование буферов (sync.Pool в Go), предварительное выделение ёмкости коллекций.
  • Следите за метриками сборщика — частотой и длительностью пауз, размером кучи — и профилируйте память (pprof в Go, tracemalloc в Python, JFR в Java).
  • В контейнерах задавайте пределы памяти, понятные среде выполнения (GOMEMLIMIT, -XX:MaxRAMPercentage), иначе процесс может быть убит по OOM раньше, чем сборщик сработает.
  • Не вызывайте сборку вручную (System.gc(), gc.collect()) без измерений — обычно это только мешает эвристикам.

См. также

Источники

  1. Jones, Hosking, Moss — The Garbage Collection Handbook (2nd ed., 2023)
  2. McCarthy — Recursive Functions of Symbolic Expressions (1960)
  3. Go — A Guide to the Go Garbage Collector
  4. Python Docs — gc module
  5. Oracle — HotSpot Virtual Machine Garbage Collection Tuning Guide
  6. V8 Blog — Trash talk: the Orinoco garbage collector