Считается, что устройство сборщика мусора в Go — знание академическое и не пригодится при написании CRUD и перекладывании JSON. Как бы не так! GC в Go устроен таким образом, что может поставить вам подножку в неожиданном месте, и придётся с ним бороться.

Привет, Хабр! Меня зовут Михаил Люц, я — ярый фанат Go, разработчик в «Ozon Банке» и решаю в основном бизнес-задачи. Эта статья написана по мотивам моего доклада для GolangConf, и сегодня мы с вами поговорим о Garbage Collector: о причинах появления, первых реализациях, эволюции алгоритмов, принципах работы GC в Gо и современных языках, которые отказались от GC.
Почему появился Garbage Collector
Если вкратце перечислить причины появления Garbage Collector, сразу можно сказать о нескольких:
ручное выделение и освобождение памяти;
двойное освобождение памяти;
утечки и фрагментация памяти;
использование освобождённой памяти;
высокая сложность разработки.
А теперь разберём подробнее каждую из них.
Ручное выделение и освобождение памяти: malloc(), calloc(), realloc(), free()
Раньше в языках вроде C и C++ мы вручную выделяли память через malloc и точно так же освобождали её через free. Звучит логично, но было много ошибок: где-то забыли free, получили утечку; где-то освободили память дважды — краш. Обратились к уже освобождённому участку памяти — снова неполадки. В итоге — принцип сапёра: один неверный шаг и всё падает.
Garbage Collector зародился именно из-за этих ситуаций. Его задача заключалась в том, чтобы позволить разработчику сфокусироваться на написании бизнес-логики, а не жить в страхе перед free.
Двойное освобождение памяти
Самая классическая ошибка в ручном выделении памяти. Предположим, есть указатель — мы его освободили через free, но по какой-то причине забыли это отметить или указатель обнулился. Опять вызываем free, как итог — двойное освобождение памяти. Результат — от непредсказуемого поведения и порчи памяти до аварийного завершения программы:
повреждение кучи (heap corruption);
исполнение произвольного кода (эксплуатация уязвимостей);
краш программы (segmentation fault / invalid free).
Особенно тяжело отлавливать это в многопоточных системах, где двойное освобождение может происходить в разных потоках. Найти такую ошибку — отдельный вид искусства. Garbage Collector, в отличие от нас, всегда знает, какой объект ещё жив, а какой не используется, и никогда не освободит один и тот же объект дважды.
Утечка памяти
Кажется, что это не критично — подумаешь, немного лишней памяти висит, не так ли? Нет.

Со временем «немного» превращается в гигабайты. Особенно это видно в long-range-сервисах, где программа должна жить неделями без перезагрузки. В кодах C и C++ мы сами отвечаем за выделение памяти. Один забытый free в цикле или утечка структуры данных — OOM Killer.
Garbage Collector системы избавляют нас от этого. Они следят за ссылками и автоматически очищают всё, что стало недостижимым. Конечно, это не панацея — ссылки могут остаться активными, даже если объект уже не нужен. Но это другая история.
Фрагментация памяти
Даже если мы правильно выделяем и освобождаем память, рано или поздно сталкиваемся с фрагментацией: в этом случае в памяти остаётся много маленьких дыр. Мы вроде бы освободили много места, но места под новый объект всё равно нет. В ручной системе фрагментация — это боль. Надо самому реализовать аллокаторы, систему уплотнения, думать об архитектуре.
Причины фрагментации:
частое выделение/освобождение блоков разных размеров;
непредсказуемый жизненный цикл объектов;
неэффективные аллокаторы памяти или отсутствие компактирования.
Последствия:
неэффективное использование памяти;
Out Of Memory (OOM), даже если свободная память есть;
ухудшение производительности из-за повышенной нагрузки на аллокатор.
Garbage Collector, особенно со стратегией Compact Garbage Collector, сами умеют переорганизовывать память и избегать этой проблемы. Для разработчиков процесс максимально прозрачный, а значит, голова не болит.
Использование освобождённой памяти
Классическая Use-after-Free (UaF). Объект уже был освобождён, память отдана системе под другой объект, а мы всё ещё держим указатель на нём и пытаемся с ним работать.
Что может пойти не так:
Segmentation fault — если память перераспределена ОС;
случайно корректная работа — это ловушка, поведение неопределённое;
возможность для атак.
Причины возникновения UaF:
доступ к указателю после free() из соображений о том, что память ещё жива;
ошибки владения памятью;
неправильное управление временем жизни объектов.
Иногда это просто краш и баги, которые проявляются раз в месяц. Такие ошибки — одни из самых сложных для отладки, особенно когда поведение меняется при каждом запуске.
Garbage Collector помогает избежать этого. Если объект ещё используется, он жив. Освобождение происходит только тогда, когда нам точно известно, что никто к нему не обращается.
Высокая сложность разработки
Всё вышеописанное приводит к одному: писать программу становится сложно на фоне того, что разработчики тратят более 50% времени на то, чтобы не упасть с ошибкой.

Garbage Collector даёт возможность сфокусироваться на логике — зачем мы пишем программы. Конечно, у него есть свои минусы: задержки, паузы, иногда непредсказуемость, накладные расходы. Но в большинстве случаев выигрыш во времени того стоит.
Первые реализации
Память — это дорогой и ограниченный ресурс, который заканчивается всегда не вовремя. Когда стало понятно, что ручное управление памятью — это ад, начали появляться первые идеи автоматического управления. Первые Garbage Collector не были сложными или универсальными. У них была одна цель — избавить разработчика от ручного выделения памяти.
Ключевые моменты и цели первых реализаций GC
К целям можно отнести автоматическое управление памятью, повышение надёжности программ, упрощение разработки сложных приложений и безопасное и эффективное повторное использование памяти.
Главное, что пытались решить первые Garbage Collector — это освобождение неиспользуемой памяти без участия разработчика. Звучит как магия: давайте найдём все объекты, которые не используются, и удалим их. На этом этапе зародились три классических алгоритма, которые легли в основу почти всех современных Garbage Collector.
Большая тройка
Алгоритм Mark-and-Sweep
Это самый первый и самый классический алгоритм.

Проходим стек от корней глобальных переменных, помечая все объекты, к которым есть доступ. Затем — по всей памяти и удаляем все объекты, которые не пометили.
Преимущества — простота (алгоритм не требует модификации объектов), подсчёт ссылок и слежение за количеством строк. Недостаток — алгоритм останавливает все приложения, пока идёт сборка. Это особенно сильно тормозит на больших кучах.
2. Алгоритм Reference Counting
Второй по популярности подход — подсчёт ссылок. Каждому объекту предписывается счётчик, сколько раз на него ссылаются. Когда счётчик обнуляется, объект освобождается или удаляется.

Кстати, это был один из первых алгоритмов, который не требовал полной остановки программы. Но у него один критичный минус — циклические ссылки. Два объекта могут ссылаться друг на друга, но при этом быть недоступны извне. Как итог — объект остаётся в памяти навсегда. Поэтому Reference Counting быстро вытеснили более гибкие подходы.
3. Алгоритм Stop-and-Copy
Он появился как попытка решить две проблемы одновременно — фрагментацию и скорость.

Память делится на две части: откуда копируем и куда. Живые объекты копируются из одной области в другую, все остальные остаются в изначальной. После этого удаляем первую область — вся мусорная память там.
Это дало два преимущества — память уплотняется и никакой фрагментации. Всё просто и быстро, потому что не нужно бегать по всей памяти. Минус — нужно в два раза больше памяти. Но на тот момент это был отличный компромисс.
Эксперименты, которые не дошли до наших дней
Возможно, они были слишком сложны, а возможно, просто неэффективны. Рассмотрим несколько из них.
Алгоритм «Директории свободных блоков» (Free-List Allocation)
Идея заключалась в том, чтобы вместо выделения и освобождения памяти вести список всех свободных блоков. Т. е. каждый раз, когда нужно выделить память, просто идём в этот список, находим блок подходящего размера и выдаём его, освобождаем, возвращаем обратно.

На первых порах это казалось разумным решением, но на практике появились минусы — список быстро засорялся кучей мелких фрагментов. Алгоритм становился долгим и требовал сложной логики управления из-за перебора, разбиения и сортировки. В итоге изначально быстрая система превращалась в хаос и её заменили более стабильные и более прогнозируемые подходы.
Линейное сканирование (Linear Scan)
Идея пришла из попыток сделать суперпростую и супербыструю сборку мусора. Принцип простой: пробегаем по всей памяти от начала до конца как по плоскости и выделяем все объекты линейно. Когда память заканчивается, начинаем сканировать её всю подряд, ищем живые объекты и решаем, что с ними делать.

Проблемы появились моментально:
Линейное выделение — это круто, пока есть память. Как только она заканчивается, начинается ад.
Нет эффективного способа освобождения памяти. Нужно либо переписывать всё, либо делать Stop-the-World (это когда мы останавливаем все потоки приложения для решения какой-то критической задачи. В нашем случае это сборка мусора).
Этот подход не масштабируется. Он хорош для мелких сервисов, но не для больших приложений.
Основные проблемы этих алгоритмов и их причины
Несмотря на то что эти алгоритмы не дожили до наших дней, они были важным этапом в эволюции. Как часто бывает в инженерии, провальные решения приближают нас к лучшим идеям. Поэтому даже забытые алгоритмы оставили свой вклад, просто их время вышло.
Эволюция алгоритмов
Изначально сборщик мусора был вынужденной мерой, чтобы избавить разработчиков от free и ошибок. Но со временем он превратился в полноценный компонент рантайма, эволюция которого шла рука об руку с прогрессией самих языков. Но росла и нагрузка.

Мы можем разделить эволюцию Garbage Collector на три поколения:
Поколение 1: простота и надёжность
На заре Garbage Collector мы запускали программу, выполняли её, останавливали, очищали от мусора и шли дальше даже с большими паузами. На тот момент для многих задач этого хватало.
Поколение 2: производительность и оптимизация
Когда появились компиляторы, игровые движки, стало понятно, что пауза в две секунды уже критична. Тогда начали эксперименты — уплотняли память, запускали GC фоново или по кусочкам, делили GC на поколения. Делали всё только для того, чтобы он стал быстрым и незаметным для пользователя.
Поколение 3: real-time и многопоточность
С приходом мобильных приложений, игр и серверов, которые работают с 60 и более FPS, стало понятно, что пауза уже в 10 мс — это неприемлемо. Поэтому Garbage Collector стал адаптивным. Возникли такие GC, как ZGC JVM, G1 и Go Garbage Collector. Это разные инструменты под разные цели, но у них один и тот же принцип — прозрачность и предсказуемость.
Идея тоже сохранилась. Если раньше Garbage Collector просто должен был работать, то теперь он стал частью инженерной стратегии. Garbage Collector влияет на архитектуру приложения. Мы выбираем язык и платформу с учётом поведения GC, иногда даже вносим правки в код, чтобы лучше работать с ним.
Эволюция большой тройки
Мы уже познакомились с алгоритмами Mark-and-Sweep, Reference Counting и Stop-and-Copy. Это были первые шаги — простые, но мощные.

По мере роста данных, потоков, требований и нагрузки стало понятно, что базовых версий больше не хватает. Эти алгоритмы начали эволюционировать.
Mark-and-Sweep
Алгоритм изначально работал грубо: останови весь мир, пройдись по памяти, убери все ненужное и т. д. Это просто, но абсолютно не подходит для современных приложений, которые не должны замирать. Так появился инкрементный Mark-and-Sweep.

Мы разбиваем сборку мусора на множество маленьких шагов, которые можно делать между кадрами, запросами и так далее. Пользователь почти не замечает паузы, всё происходит постепенно.
Затем появился конкурентный Mark-and-Sweep — конкурентный GC. Фаза Mark теперь запускается параллельно с выполнением программы, требует более сложных механизмов синхронизации, но даёт отличную отзывчивость.
В итоге Mark-and-Sweep стал умнее, он больше не блокирует весь мир и теперь работает даже в real-time-системах.
Reference Counting
Базовый Reference Counting умер из-за циклов, но подход жив как никогда — просто в иной форме. Давайте вспомним object C, Swift — там активно используется автоматика Reference Counting (ARC). Компилятор сам встраивает инструкции увеличения и уменьшения счётчиков, работает быстро и эффективно, особенно если нет циклов. В C это умные указатели, а в некоторых системах Reference Counting используется в связке с Garbage Collector. Например, считает ссылки на локальном уровне, а если не сработало, то запускает полноценную сборку мусора.
В итоге Reference Counting не исчез, а перешёл в локальную, более гибкую форму, где его слабые стороны более контролируемы.
Stop-and-Copy
Алгоритм стал основой для одного из самых успешных направлений в эволюции Garbage Collector — поколенческих сборщиков мусора. Идея заключается в том, что только созданные объекты, скорее всего, быстро умрут, а те, что живут долго, — будут жить ещё дольше. На этом построена структура:
Молодое поколение — объекты, которые только что появились, их легко быстро скопировать и удалить.
Старое поколение — сборка запускается гораздо меньше.
При этом молодое поколение очищается по принципу Stop-and-Copy. Этот гибрид объединил лучшее из обоих миров. В итоге Stop-and-Copy стал сердцем поколенческого подхода, который используется в JVM, .NET, Go и многих других системах.
Алгоритмы работы Garbage Collector в Go
У истоков стояли простота, скорость и надёжность. Сборщик мусора был построен в том же духе — простой Stop-the-World, простой Mark-and-Sweep.
Рассмотрим, как это работало. Всё приложение остановилось, GC делает полный обход памяти, находит все живые объекты, помечает их и очищает остатки. Просто и надёжно, но при этом жёстко и с задержками — паузы могли доходить до 200−300 мс, особенно при больших массивах или сложных структурных данных. В микросервисах это превращалось в реальную проблему — задержки на обработку запросов, скачки latency, нестабильность под нагрузкой. Но на старте Go это был приемлемый компромисс.
Развитие Garbage Collector в Go можно разделить на четыре этапа: Go 1.0, Go 1.5, Go 1.9 и Go 1.20+.

Golang 1.0
Первая стабильная версия Go 1.0, вышедшая в 2012 году, унаследовала ту же простую модель Garbage Collector. Это был абсолютно классический GC, который пробегал по всей куче и удалял неиспользуемые объекты. Для того времени это было нормой. Конкуренты вроде Java или .NET имели более продвинутые GC, но весили куда больше.
Go делал ставку на минимализм, быстроту компиляции и независимость от виртуальных машин. Он был грубый и тормозил, но Go давал возможность писать эффективные CLI, простые сервисы, скрипты, где задержки не так критичны. Это был базис, от которого команды языка отталкивалась, чтобы двигаться к серьёзным задачам.
Golang 1.5
Это была революция. Версия Go 1.5, вышедшая в 2015 году, буквально переизобрела сборщик мусора. Вместо классической Stop-the-World-модели был реализован конкурентный Mark Garbage Collector. Программа теперь не останавливается полностью на время пометки, а работает параллельно, сканируя указатели, даже пока код пользователя выполняется. Это был первый большой шаг к low latency.
Паузы уменьшились с сотен миллисекунд до десяток, а в некоторых случаях и до единиц. Под капот добавили защитный механизм write barriers, чтобы Garbage Collector не терял ссылки, когда объект модифицируется. Также обновили рантайм, чтобы работать с кучей многопоточности.
Garbage Collector начал активно использоваться в сервисных приложениях, REST API, микросервисах, где уже каждая миллисекунда важна. Go начал встраиваться в такие сценарии. Но, что самое главное, это всё работает автоматически. Разработчику не нужно ничего настраивать — он просто пишет код, а Garbage Collector работает фоново.
Golang 1.9+
В Go 1.9 и в следующих версиях мы начали шлифовать Garbage Collector до блеска. Основная цель — сделать его умным и незаметным.
Появились точное отслеживание указателей, фоновая сборка мусора, микропаузная модель, адаптивность. Garbage Collector начал подстраиваться под текущие загрузки и учитывать размер кучи и активности программ. Это был не просто тюнинг, а подход от простого конкурентного Garbage Collector к интеллектуальной системе памяти, которая умеет думать и реагировать.
Golang 1.20+
Наконец, наши дни. В версиях Go 1.18–1.21 сборщик мусора стал практически невидимым.
Паузы уменьшились до одной миллисекунды — даже на больших объёмах памяти. Он сам определяет оптимальное время и интенсивность работы. Мы получили настройку GOMEMLIMIT — теперь Garbage Collector на основе заданного лимита выбирает, как и когда ему работать. Появились расширенные метрики, позволяющие в деталях отследить поведение Garbage Collector и оптимизировать производительность. Это уже почти real-time Garbage Collector, подходящий даже для стриминго-игровых сервисов и систем, где задержка критична. Это всё без настройки.
Мы так же пишем код, а Garbage Collector делает своё дело.
Выводы
Garbage Collector в Go прошёл путь от лопаты к автопилоту. Когда-то это был простой ручной трактор, а сегодня это умная адаптивная модель, которая работает в фоновом режиме и почти не трогает код. Ниже — сравнение GC по версиям.

Современные языки, которые отказались от GC
Мы поговорили о пути GC от доисторических malloc и free, через Mark-and-Sweep и Stop-the-World до современных почти real-time Garbage Collector в Go и Java. Мы победили утечки, научились собирать мусор фоново, сократили паузы до миллисекунд. Вроде бы всё хорошо, но тут приходит Rust и говорит: «Ребята, а зачем вообще сборщик мусора?»
Поговорим о двух современных языках, которые отказались от Garbage Collector, — это Rust и Carbon.
Rust
Это язык без Garbage Collector и free, но с абсолютной скоростью. Будто кто-то сказал: «Давайте делать всё как в C, только безопасно, без сборщика мусора, без пауз, но при этом без ошибок в памяти, утечек и use-after-free». И у них получилось.
Вместо Garbage Collector Rust использует модель владения памятью Ownership, встроенную прямо в компилятор. Всё решается прямо на этапе компиляции, что даёт феноменальные преимущества: никакой сборки мусора, а значит, никаких пауз, ручной работы с free и ошибок. Мы просто описываем объекты, и компилятор всё сам контролирует.
Модель основана на трёх китах:
У каждого ресурса есть владелец.
Владелец отвечает за очистку.
При передаче владения прежний доступ к ресурсу блокируется.

У Rust нет рантайма, фоновых задач в сборке мусора, аллокации лишней памяти. Вся она чистится сразу, когда выходит из области видимости. Никаких отложенных процессов — всё происходит в compile time. Идеально для системного программирования.
Rust отлично подходит там, где нужна максимальная производительность, нулевые задержки, жёсткий контроль над ресурсами — игры, базы данных, операционные системы. Rust захватывает те области, где даже микропауза из-за GC была бы неприемлема.
Но есть нюанс. Маленький пример кода на Rust:

За всё приходится платить, и в Rust мы платим умственной нагрузкой. Компилятор нам не прощает ошибок: входной порог выше, простые вещи могут занять больше времени. Но взамен мы получаем железную безопасность и контроль уровня C с удобствами уровня GO (почти).
Carbon
Здесь мы должны были поговорить о том, как он эволюционировал и стал крутым языком без Garbage Collector, но, к сожалению, ребята так и не довели его до стабильной версии 1.0. Пока нам нужно знать, что был такой язык, который тоже отказался от Garbage Collector.
Итог
Garbage Collector в Go и Java — это про гибкость, простоту и минимальное участие разработчиков. Rust — это про жёсткий контроль, ноль пауз и максимальную эффективность.
Нас действительно обошли в абсолютной скорости, но не в универсальности и лёгкости разработки. Как бы ни было, теперь есть выбор — это самое крутое в современном программировании. А что бы выбрали вы?
В апреле 2026 года в Москве пройдёт Golang Conf — конференция при участии ведущих Go-разработчиков. Участников ожидают полезные доклады и практические знания от лучших спикеров, митапы и много нетворкинга. Вы сможете завести полезные знакомства, а может, даже придумаете что-то прорывное и инновационное в ходе дискуссий с коллегами. Следите за обновлениями на сайте!
Мой тг канал - https://t.me/GoGopherss
