Сборка мусора vs ручное управление памятью
| Ручное управление | C (malloc/free) |
|---|---|
| RAII и умные указатели | C++, Rust |
| Подсчёт ссылок | CPython, Swift (ARC), Rust (Rc, Arc) |
| Трассирующая сборка мусора | Go, Java, C#, JavaScript |
| Владение и заимствование | Rust (проверка при компиляции) |
| Первая сборка мусора | Lisp, Джон Маккарти (1959–1960) |
| Цена ошибок | около 70% уязвимостей в крупных кодовых базах на C и C++ связаны с памятью |
Управление памятью — способ, которым программа получает память для данных и возвращает её, когда данные больше не нужны. Языки решают эту задачу по-разному: в C программист освобождает память вручную, в Go, Java и Python этим занимается сборщик мусора, а Rust проверяет правила владения при компиляции и освобождает память автоматически без сборщика. Выбор подхода определяет безопасность, производительность, предсказуемость задержек и удобство языка.
Проблема
Данные с неизвестным заранее временем жизни размещаются в куче. Их нужно освободить ровно один раз и ровно тогда, когда никто больше ими не пользуется. Ошибки приводят к тяжёлым последствиям:
| Ошибка | Что происходит |
|---|---|
| Утечка памяти | память не освобождается, процесс постепенно разрастается |
| Использование после освобождения (use-after-free) | чтение или запись в чужие данные, уязвимости |
| Двойное освобождение | повреждение структур аллокатора |
| Выход за границы буфера | перезапись соседних данных, классический способ взлома |
По данным Microsoft и проекта Chromium, около 70% серьёзных уязвимостей в их кодовых базах на C и C++ связаны именно с безопасностью памяти.[4][5] Поэтому государственные агентства и крупные компании рекомендуют переходить на языки с безопасной памятью.
Подходы
Ручное управление
В C память выделяют malloc и освобождают free. Это даёт полный контроль и предсказуемость, но вся ответственность лежит на программисте, и ошибки из таблицы выше неизбежны в больших программах. Инструменты — AddressSanitizer, Valgrind — помогают находить их при тестировании.
RAII и умные указатели
В C++ ресурс привязывается к времени жизни объекта (Resource Acquisition Is Initialization): деструктор освобождает память, закрывает файл или снимает блокировку при выходе из области видимости. Умные указатели unique_ptr (единственный владелец) и shared_ptr (подсчёт ссылок) избавляют от большинства ручных delete, но язык не запрещает небезопасные приёмы.
Подсчёт ссылок
У каждого объекта есть счётчик ссылок; когда он падает до нуля, объект немедленно освобождается. Так работает CPython, Swift (автоматический подсчёт ссылок, ARC) и типы Rc/Arc в Rust. Плюсы — предсказуемое освобождение без пауз. Минусы — накладные расходы на каждое присваивание и циклические ссылки: два объекта, ссылающиеся друг на друга, никогда не освободятся. CPython дополняет подсчёт ссылок циклическим сборщиком мусора.[3]
Трассирующая сборка мусора
Идею автоматической сборки мусора предложил Джон Маккарти для языка Lisp в 1959–1960 годах.[1] Сборщик периодически находит все объекты, достижимые от «корней» — глобальных переменных и стека, — а остальные считает мусором. Варианты:[6]
- Пометка и очистка (mark-sweep) — пометить достижимое, освободить остальное.
- Поколенческая сборка — большинство объектов умирает молодыми, поэтому молодое поколение проверяется часто, старое — редко (Java, .NET, V8).
- Конкурентная сборка — основная работа выполняется параллельно с программой, паузы сокращаются до долей миллисекунды. Сборщик Go — конкурентный трёхцветный mark-sweep, настраиваемый параметрами
GOGCиGOMEMLIMIT.[2]
Плюсы — простота для программиста и безопасность: невозможно обратиться к освобождённой памяти. Минусы — дополнительная память (сборщику нужен запас), паузы и затраты процессора, менее предсказуемые задержки. Для большинства серверных приложений это приемлемо; для систем жёсткого реального времени, ядер ОС и игровых движков — часто нет.
Владение и заимствование
Rust применяет правила владения на этапе компиляции: у каждого значения один владелец, при выходе владельца из области видимости память освобождается (как RAII), а ссылки-заимствования не могут пережить данные, на которые указывают. Ошибки использования после освобождения и гонки данных отсекаются компилятором, а во время выполнения нет ни сборщика, ни накладных расходов. Цена — более сложное обучение и необходимость выражать структуры с общими ссылками через Rc, Arc и индексы.
Сравнение
| Ручное (C) | RAII (C++) | Подсчёт ссылок | Трассирующий GC (Go, Java) | Владение (Rust) | |
|---|---|---|---|---|---|
| Безопасность памяти | нет | частичная | да | да | да (вне unsafe) |
| Паузы | нет | нет | нет (кроме сборки циклов) | короткие, но есть | нет |
| Накладные расходы | минимальные | минимальные | на каждое присваивание | память и процессор сборщика | минимальные |
| Сложность для программиста | высокая | средняя | низкая | низкая | средняя или высокая |
| Утечки | возможны | возможны | при циклах | редки | возможны, но редки |
Как выбирать
- Сетевые сервисы, инструменты, бизнес-логика — языки со сборкой мусора (Go, Java, C#, Python) дают высокую продуктивность при достаточной скорости.
- Системное программирование, встраиваемые системы, низкие и предсказуемые задержки, библиотеки для других языков — Rust, а в существующих кодовых базах — C и C++.
- Утечки возможны в любом языке: кэш без ограничения размера или забытые подписки удерживают объекты и под сборщиком мусора.
См. также
Источники
- J. McCarthy — Recursive Functions of Symbolic Expressions and Their Computation by Machine (CACM, 1960)
- A Guide to the Go Garbage Collector
- Python — gc: Garbage Collector interface
- Microsoft Security Response Center — A proactive approach to more secure code (2019)
- The Chromium Projects — Memory safety
- R. Jones, A. Hosking, E. Moss — The Garbage Collection Handbook