Как работает сборка мусора в Java, Go и JavaScript
Продвинутое3 минРедакция Innovialabs
Коротко
Сборщик мусора освобождает объекты, до которых программа больше не может дотянуться по ссылкам; утечки возникают, когда ненужный объект всё ещё достижим, например из кэша или подписки.
Коротко: что делает сборщик мусора
Garbage collector (GC) автоматически освобождает память, которую программа больше не использует. Ключевое слово — достижимость: объект жив, пока до него можно добраться по цепочке ссылок от корней (локальные переменные, стек, глобальные и статические поля). Всё недостижимое — мусор.
Отсюда главный практический вывод: GC не знает, что объект вам «больше не нужен». Он знает только, что на него никто не ссылается. Поэтому утечки памяти в языках с GC — обычное дело.
Три базовых подхода
Подсчёт ссылок (reference counting)
У каждого объекта есть счётчик ссылок. Когда он падает до нуля, объект освобождается сразу.
- Плюс: память освобождается предсказуемо и немедленно.
- Минус: циклические ссылки (A ссылается на B, B на A) никогда не обнулятся. Поэтому CPython дополняет подсчёт отдельным сборщиком циклов, а в Swift (ARC) разработчик разрывает циклы через
weak.
Ни Java, ни Go, ни V8 не используют подсчёт ссылок как основной механизм.
Mark-and-sweep
- Mark — обход графа объектов от корней, все достижимые помечаются.
- Sweep — непомеченные объекты освобождаются.
Циклы здесь не проблема: если до цикла нельзя добраться от корней, он целиком уходит в мусор. Часто добавляется compaction — уплотнение живых объектов, чтобы убрать фрагментацию.
Поколения (generational GC)
Наблюдение, на котором всё держится: большинство объектов умирает молодыми. Поэтому куча делится на молодое и старое поколение. Молодое собирается часто и быстро (обычно копированием выживших), старое — реже. Объект, переживший несколько сборок, переезжает в старое поколение.
Как это сделано в трёх рантаймах
| Java (HotSpot) | Go | JavaScript (V8) | |
|---|---|---|---|
| Основа | Поколения + mark/compact | Конкурентный tri-color mark-and-sweep | Поколения + mark-sweep-compact |
| Поколения | Да | Нет | Да |
| Перемещает объекты | Да | Нет | Да |
| Настройка | Выбор сборщика, размеры кучи | GOGC, GOMEMLIMIT | Флаги V8, в основном не трогают |
Java. В JVM есть несколько сборщиков: G1 (по умолчанию в современных JDK), низколатентные ZGC и Shenandoah, а также Parallel и Serial. Выбор — это компромисс между паузами, пропускной способностью и потреблением памяти.
Go. Сборщик конкурентный: он работает параллельно с программой и делает короткие паузы. Поколений и перемещения нет. Зато компилятор через escape analysis размещает многие значения на стеке, и они вообще не нагружают GC. Частоту сборок задаёт GOGC, мягкий лимит памяти — GOMEMLIMIT.
JavaScript (V8). Молодое поколение собирается быстрым копирующим сборщиком (Scavenger), старое — mark-sweep-compact, большая часть работы выполняется инкрементально и в фоновых потоках. Другие движки устроены иначе, но идея поколений распространена.
Как утечки всё равно происходят
Утечка в языке с GC — это ненужный, но достижимый объект. Типичные причины:
- Java: статические коллекции и кэши без вытеснения, незакрытые подписки и listeners,
ThreadLocalв пулах потоков. - Go: горутины, которые навсегда заблокированы на канале; маленький срез, держащий огромный исходный массив; бесконечно растущие map.
- JavaScript: забытые
addEventListenerиsetInterval, замыкания, захватившие большие данные, отсоединённые DOM-узлы, которые ещё где-то хранятся.
Общая закономерность: всё, что живёт долго (глобальные структуры, синглтоны, кэши), должно иметь ограничение или явную очистку.
Как найти утечку
- Подтвердите рост. Память после сборки мусора растёт от часа к часу при стабильной нагрузке — тревожный сигнал. Пик сам по себе — нет.
- Снимите два снимка кучи с интервалом и сравните, какие объекты прибавились.
- Найдите путь удержания — цепочку ссылок от корня к «лишнему» объекту. Именно она показывает, кто держит память.
Инструменты:
- Java:
jcmd <pid> GC.heap_dump, анализ в Eclipse MAT или VisualVM, Java Flight Recorder. - Go:
net/http/pprofиgo tool pprofдля профиля кучи, профиль горутин для поиска зависших. - JavaScript: вкладка Memory в Chrome DevTools (heap snapshots, сравнение), для Node.js —
node --inspect.
import _ "net/http/pprof" // добавит /debug/pprof/ на ваш HTTP-сервер