Сборка мусора
| Появление | Lisp, Джон Маккарти (1959) |
|---|---|
| Подходы | подсчёт ссылок, трассировка (mark-sweep, mark-compact, копирование) |
| Улучшения | поколения, инкрементальная и конкурентная сборка |
| Языки с GC | Java, C#, Go, Python, JavaScript, Ruby, Haskell |
| Без GC | C, 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()) без измерений — обычно это только мешает эвристикам.
См. также
Источники
- Jones, Hosking, Moss — The Garbage Collection Handbook (2nd ed., 2023)
- McCarthy — Recursive Functions of Symbolic Expressions (1960)
- Go — A Guide to the Go Garbage Collector
- Python Docs — gc module
- Oracle — HotSpot Virtual Machine Garbage Collection Tuning Guide
- V8 Blog — Trash talk: the Orinoco garbage collector