Сборка мусора vs ручное управление памятью

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Сборка мусора 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++.
  • Утечки возможны в любом языке: кэш без ограничения размера или забытые подписки удерживают объекты и под сборщиком мусора.

См. также

Источники

  1. J. McCarthy — Recursive Functions of Symbolic Expressions and Their Computation by Machine (CACM, 1960)
  2. A Guide to the Go Garbage Collector
  3. Python — gc: Garbage Collector interface
  4. Microsoft Security Response Center — A proactive approach to more secure code (2019)
  5. The Chromium Projects — Memory safety
  6. R. Jones, A. Hosking, E. Moss — The Garbage Collection Handbook