awesome-everything EN
↑ Обратно к восхождению

Производительность

Внутреннее устройство GC: tri-color инвариант, write barriers и глубокое погружение в рантаймы

Суть Tri-color маркировка — формальная основа любого concurrent GC. Write barriers поддерживают инвариант. Go''''s pacer, colored pointers ZGC и V8 Orinoco реализуют одни идеи по-разному — знание реализации определяет, как писать allocation-safe hot paths.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 18 min

JVM-сервис мигрирует с G1 на ZGC. Паузы падают с 60 мс до субмиллисекундных на 16 ГБ кучи — но throughput снижается на 12%, а потребление памяти растёт на 18%. Чтобы понять почему, нужно знать, что такое colored pointers и сколько стоят load barriers.

Tri-color маркировка и write barrier

Tri-color абстракция (Dijkstra, 1978) — формальный фундамент concurrent GC. Объекты классифицируются в три цвета:

  • Белый — ещё не посещён; кандидат для сборки, если маркировка закончится, пока объект белый.
  • Серый — посещён, но дочерние объекты ещё не полностью просканированы.
  • Чёрный — посещён, все дочерние объекты просканированы; считается живым.

Маркировка переводит серые объекты в чёрные, сканируя их дочерние объекты и делая каждый непосещённый дочерний серым. Когда серых объектов не остаётся, все белые — недостижимы и могут быть возвращены.

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

SATB vs incremental-update барьеры

Write barrier предотвращает нарушение инварианта, перехватывая каждую запись ссылки:

Snapshot-at-the-beginning (SATB): барьер маркирует старую ссылку, которую вот-вот перезапишут, обеспечивая её выживание в текущем цикле. Коллектор ведёт себя так, как если бы снял снимок кучи на старте GC. Используется в G1, Shenandoah, ZGC и гибридном Yuasa-барьере Go.

Incremental-update (стиль Dijkstra): барьер маркирует новую ссылку, записываемую в поле чёрного объекта, гарантируя её сканирование до завершения цикла. Используется в CMS и классическом V8 mark-compact.

SATB консервативнее — может сохранить объекты, ставшие мусором во время цикла (floating garbage, возвращается в следующем цикле). Но даёт более строгие гарантии завершения маркировки и проще в формальном анализе. Incremental-update может требовать фазы повторной маркировки.

Оба варианта стоят 2–10% CPU на каждой записи ссылки — цена concurrent маркировки без больших STW-пауз.

Тип барьераЧто маркируетИспользуется вПобочный эффект
SATBСтарая ссылка (до записи)G1, Shenandoah, ZGC, GoFloating garbage (задержка на один цикл)
Incremental-updateНовая ссылка (после записи)CMS, классический V8Может потребоваться фаза remark
Почему это работает

Write barriers важны для write-heavy hot paths. Сервис, записывающий миллионы ссылок в секунду (например, обновляющий большой in-memory граф), платит стоимость барьера при каждой записи. Для большинства CRUD-сервисов это незначительно; для graph-mutation или event-sourcing workloads это появляется в профилях как runtime.wbBufFlush (Go) или аналогичные GC-фреймы. Знайте свой паттерн записей, прежде чем утверждать, что барьер бесплатен.

Переработка pacer Go

Переработка GC pacer Go в 1.18 (proposal 44167, Михаил Кнышек) заменила эвристики на замкнутую систему управления. Старый pacer оценивал, когда запустить следующий цикл, чтобы успеть до удвоения кучи; он был нестабильным при высоком rate аллокаций и принимал плохие решения при cgo-heavy workloads.

Новый pacer использует PI-контроллер (пропорционально-интегральный) по двум сигналам: rate роста кучи и утилизация CPU в GC. Контроллер нацеливается на завершение GC до достижения целевого размера кучи (заданного GOGC), с интегральной обратной связью, предотвращающей устойчивое отклонение.

GOMEMLIMIT (добавлен в 1.19) интегрирован в pacer: по мере приближения к лимиту pacer вытягивает GC вперёд — принимая бо́льший GC CPU — для предотвращения OOM. При нахождении ниже лимита pacer отступает.

Production-совет: устанавливать GOMEMLIMIT в ~90% лимита контейнера; оставлять GOGC на дефолтном 100 если только профилирование не показывает конкретную причину для изменения. GOGC=off безопасен только для memory-bounded batch-задач, освобождающих память через завершение процесса.

Переработка сократила дисперсию пауз ~на 50% на реальных workloads.

ZGC и colored pointers

ZGC (JEP 333, JDK 11 экспериментальный; production в JDK 15 через JEP 377) достигает субмиллисекундных пауз на кучах до 16 ТБ за счёт двух инноваций:

Colored pointers упаковывают метаданные в 64-битный указатель. ZGC использует биты 0–41 для адреса (ограничивая кучу ~4 ТБ) и биты 42–45 для состояния маркировки — «хорошие» цвета vs «плохие», указывающие на перемещение или ожидающую работу.

Load barriers перехватывают каждую загрузку из кучи (каждое разыменование указателя). Если цвет «плохой», барьер запускает медленный путь для обновления указателя на месте. Поскольку барьер выполняется inline при каждой загрузке, приложение участвует в работе GC инкрементально, а не ждёт большой STW-фазы.

Результат: маркировка, перемещение и обработка ссылок — всё concurrent. STW-фазы ограничены сканированием корней — субмиллисекундные даже на кучах в несколько ТБ.

Tradeoff: load barriers стоят ~5–15% CPU на read-heavy workloads. ZGC также требует multi-mapped кучи для быстрого перемещения, сильно раздувая виртуальную память (но не физический RSS). Падение throughput на 12% и рост памяти на 18% в hook-сценарии — ожидаемые расходы ZGC, не баги.

Generational ZGC (JEP 439, JDK 21+) добавляет молодое поколение, сокращая бо́льшую часть разрыва в throughput с G1. Команды, переходящие на JDK 21+, должны оценить generational ZGC.

V8 Orinoco

Проект V8 Orinoco (2017+) перевёл GC V8 с преимущественно stop-the-world на преимущественно concurrent:

  • Concurrent marking: маркировка выполняется в фоновом потоке параллельно с JavaScript. Write barriers (SATB-стиль) поддерживают согласованность.
  • Parallel compaction: несколько потоков перемещают объекты параллельно во время STW-фазы компактизации, сокращая её длительность.
  • Параллельный scavenger молодого поколения: несколько потоков эвакуируют молодую кучу параллельно.

Результат: типичные web workloads видят паузы ≤10 мс, большая часть маркировки скрыта в фоне. Overhead памяти: ~5–15% для инфраструктуры маркировки.

Node.js наследует Orinoco по умолчанию. Настройка через --max-old-space-size и --max-semi-space-size. Крупные изменения Orinoco могут сместить характеристики производительности при обновлении Node — инженерные команды должны отслеживать release notes V8.

Викторина

Сервис мигрировал с G1 на ZGC: паузы упали с 60 мс до <1 мс, но throughput снизился на 12%, а RSS вырос на 18%. Это ожидаемо?

Викторина

Почему GC Go использует SATB write barrier вместо incremental-update?

Расставь шаги по порядку

Расставьте шаги, которые load barrier ZGC выполняет при чтении указателя с 'плохим' цветом:

  1. 1 Мутатор читает ссылку из кучи (разыменование указателя)
  2. 2 Inline load barrier проверяет биты цвета указателя
  3. 3 Цвет 'плохой' — объект перемещён или ожидает обработки
  4. 4 Барьер запускает медленный путь: ищет в таблице forwarding
  5. 5 Барьер обновляет указатель на месте до нового адреса
  6. 6 Мутатор продолжает работу с исправленным (heal'd) указателем
Вспомните перед уходом
  1. 01
    Объясните tri-color инвариант и роль write barrier в его поддержании при concurrent маркировке.
  2. 02
    Какую проблему решила переработка pacer Go 1.18 и какова роль GOMEMLIMIT?
Итог

Tri-color маркировка классифицирует объекты как белые, серые и чёрные и поддерживает инвариант: ни один чёрный объект не должен напрямую ссылаться на белый. Write barrier обеспечивает этот инвариант при concurrent маркировке, перехватывая каждую запись ссылки: SATB маркирует старую ссылку (используется в Go, G1, ZGC); incremental-update — новую (в CMS, классическом V8). Оба стоят 2–10% CPU. ZGC расширяет это colored pointers — метаданными в 64-битных указателях — и load barriers, исправляющими устаревшие указатели inline, достигая субмиллисекундных пауз ценой ~5–15% throughput и повышенной памяти. Переработка pacer Go (1.18) заменила эвристики PI-контроллером; GOMEMLIMIT (1.19) даёт контейнеризованным сервисам мягкий лимит памяти, который pacer соблюдает. Orinoco V8 привнёс concurrent маркировку и parallel compaction, снижая паузы JavaScript GC до ≤10 мс. Знание барьера вашего рантайма определяет, как писать write-heavy hot paths.

Связанные уроки
встречается в159
Продолжить восхождение ↑GC в production: наблюдаемость, безопасность, edge cases и управление флотом
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources6
expand
  1. 01
  2. 02
  3. 03
  4. 04
  5. 05
  6. 06

Trademarks belong to their respective owners. Editorial reference only.