Перейти к содержанию
Innovialabs

Как работает сборка мусора в Java, Go и JavaScript

Продвинутое3 мин

Коротко

Сборщик мусора освобождает объекты, до которых программа больше не может дотянуться по ссылкам; утечки возникают, когда ненужный объект всё ещё достижим, например из кэша или подписки.

Коротко: что делает сборщик мусора

Garbage collector (GC) автоматически освобождает память, которую программа больше не использует. Ключевое слово — достижимость: объект жив, пока до него можно добраться по цепочке ссылок от корней (локальные переменные, стек, глобальные и статические поля). Всё недостижимое — мусор.

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

Три базовых подхода

Подсчёт ссылок (reference counting)

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

  • Плюс: память освобождается предсказуемо и немедленно.
  • Минус: циклические ссылки (A ссылается на B, B на A) никогда не обнулятся. Поэтому CPython дополняет подсчёт отдельным сборщиком циклов, а в Swift (ARC) разработчик разрывает циклы через weak.

Ни Java, ни Go, ни V8 не используют подсчёт ссылок как основной механизм.

Mark-and-sweep

  1. Mark — обход графа объектов от корней, все достижимые помечаются.
  2. Sweep — непомеченные объекты освобождаются.

Циклы здесь не проблема: если до цикла нельзя добраться от корней, он целиком уходит в мусор. Часто добавляется compaction — уплотнение живых объектов, чтобы убрать фрагментацию.

Поколения (generational GC)

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

Как это сделано в трёх рантаймах

Java (HotSpot)GoJavaScript (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-узлы, которые ещё где-то хранятся.

Общая закономерность: всё, что живёт долго (глобальные структуры, синглтоны, кэши), должно иметь ограничение или явную очистку.

Как найти утечку

  1. Подтвердите рост. Память после сборки мусора растёт от часа к часу при стабильной нагрузке — тревожный сигнал. Пик сам по себе — нет.
  2. Снимите два снимка кучи с интервалом и сравните, какие объекты прибавились.
  3. Найдите путь удержания — цепочку ссылок от корня к «лишнему» объекту. Именно она показывает, кто держит память.

Инструменты:

  • 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-сервер

Частые вопросы

Технически да (System.gc(), runtime.GC()), но в продакшене это почти никогда не решает проблему. Если память растёт, нужно искать удерживающие ссылки, а не форсировать сборку.

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.