Перейти к основному содержимому

Java, Python и Go — три модели GC

Разработчику Архитектору

Зачем сравнивать

Сборка мусора (Garbage Collection, GC) — автоматическое освобождение памяти, занятой объектами, которые программа больше не использует. Сборщик ищет недостижимые объекты (нет цепочки ссылок от корней — стеков потоков, глобальных переменных, регистров) и возвращает их память в пул.

Во всех трёх языках цель одна, а реализация разная. Понимание модели конкретного runtime помогает объяснить паузы, утечки и выбор настроек под нагрузку.

Где углубиться

Общая теория, утечки через «живые» ссылки, C# и детальная настройка JVM — в автоматическом управлении памятью. Ниже — сжатое сравнение Java, Python и Go в духе учебной шпаргалки GC 101.


Сводная таблица

Java (JVM)Python (CPython)Go (runtime)
Основная идеяТрассировка достижимости из корнейПодсчёт ссылок + циклический GCКонкурентный mark-and-sweep
Поколения кучиYoung (Eden, Survivor) + Old; Metaspace вне heapУ gc — три поколения только для циклов; основной путь — refcountПоколений нет — единая куча
Типичные паузыОт миллисекунд (G1) до субмиллисекунд (ZGC)STW при проходе gc; refcount без паузыЧасто < 100 мкс, цель — низкая латентность
Настройка-XX:+UseG1GC, -XX:+UseZGC, -Xmxgc.set_threshold, gc.collect()GOGC, GODEBUG=gctrace=1
Практика в разделеJVMCPython и памятьОсновы Go

Достижимость — общая схема

Во всех трёх средах «мусор» — объект, до которого нельзя дойти по ссылкам от корней.

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


Java — поколения и выбор алгоритма

Память JVM

ОбластьНазначение
Heap — Young GenEden + два Survivor (S0/S1); новые объекты и Minor GC
Heap — Old GenДолгоживущие объекты после нескольких сборок
Non-Heap — MetaspaceМетаданные классов (с Java 8 вместо PermGen)

Гипотеза поколений: большинство объектов умирают молодыми; полный обход всей кучи нужен реже.

Эволюция сборщиков (упрощённо)

СборщикИдеяКогда смотреть
SerialОдин поток, STWМалые heap, встраиваемые системы
ParallelНесколько потоков, упор на throughputПакетная обработка, допустимы редкие длинные паузы
CMSЧастично конкурентный mark-sweepУстарел (удалён с JDK 14)
G1Куча регионами; сначала регионы с «мусором»По умолчанию с JDK 9, -XX:MaxGCPauseMillis
ZGCНизкие паузы, colored pointers, load barriersИнтерактивные сервисы, большие heap

G1 и регионы

G1 делит heap на регионы фиксированного размера (обычно 1–32 МБ). Роль региона меняется:

МеткаРоль
E (Eden)Создание новых объектов
S (Survivor)Объекты, пережившие young-сборку
O (Old)Старое поколение

Сборщик выбирает регионы с высокой долей мусора — отсюда имя Garbage-First.

Пример запуска с G1 и логом:

java -XX:+UseG1GC -Xmx4g -Xlog:gc*:file=gc.log -jar app.jar

Подробнее — JVM и сборщик мусора, настройка в статье 1 и шпаргалка JVM Options.


Python — подсчёт ссылок и циклы

Два уровня

  1. Reference counting — у каждого PyObject есть ob_refcnt. Счётчик 0 → объект освобождается сразу, без отдельной «волны» GC.
  2. Модуль gc — mark-and-sweep для циклических ссылок (два контейнера ссылаются друг на друга, но недостижимы снаружи).
import sys

a = []
b = [a]
a.append(b) # refcount > 0 у обоих, снаружи недостижимы
del a, b
import gc
gc.collect() # разрывает цикл

Mark-and-sweep в gc

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

Три поколения (0, 1, 2) ускоряют проверку циклов: молодые контейнеры сканируют чаще. Это дополнение к refcount, а не замена поколенческой JVM-кучи.

Пулы памяти (pymalloc)

Малые объекты (< 512 байт) часто берутся из pymalloc: арены делятся на блоки в состояниях untouched, free, allocated. Крупные объекты идут в системный malloc. Пулы снижают фрагментацию и ускоряют аллокации, но долгоживущий процесс всё равно может раздувать RSS — см. архитектуру выполнения Python.


Go — конкурентный mark-and-sweep без поколений

Рантайм Go встроен в бинарник. Сборщик конкурентный: большая часть mark-and-sweep идёт параллельно с горутинами; короткие STW — на root scan и согласование фаз.

Трёхцветная разметка

ЦветСмысл
БелыйЕщё не просмотрен или недостижим
СерыйПросмотрен, дети ещё не обработаны
ЧёрныйПросмотрен вместе с исходящими ссылками

Корни (стек горутин, глобалы, регистры) «серые»; обход переводит объекты в чёрные; оставшиеся белые — мусор.

Гибридный write barrier

Пока mutator (ваш код) меняет указатели, collector не должен потерять живой объект. Go использует insert и delete барьеры при записи в указатели — runtime вставляет проверки при компиляции.

Поколений в Go нет: проще модель, предсказуемая настройка через GOGC (процент роста heap до следующего цикла, по умолчанию 100).

GOGC=50 ./myapp # чаще GC, меньше heap
GODEBUG=gctrace=1 ./myapp # лог циклов в stderr

Практика снижения аллокаций — sync.Pool, вынос make из циклов — в основах Go.


Как выбрать фокус обучения

ЦельС чего начать
Сервер на JVM, тюнинг паузJava в этой статье → 1.md, Java23.md
Долгий Python-процесс, циклы ссылокPython выше → 27.md
Микросервис на Go, GC pressureGo выше → 13.md
C#, Unity, .NET1.md, .NET

Краткий итог

  • Java — богатый выбор GC, поколенческая heap, регионы G1, низколатентные ZGC/Shenandoah.
  • Python — в первую очередь refcount; gc ловит циклы и использует поколения для этих проходов; pymalloc ускоряет мелкие объекты.
  • Go — один конкурентный mark-and-sweep, трёхцветная схема, write barrier, настройка GOGC, без generational heap.

Во всех случаях GC освобождает только недостижимое; семантику «нужен / не нужен» задаёт архитектура ссылок в коде.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").