Компиляторы

Продвинутый GC: concurrent и real-time

Discord переходил с Go на Rust в 2020 году и одной из причин была непредсказуемость GC паузы Go: каждые 2 минуты - pause на несколько миллисекунд, заметная пользователям. Но в 2024 году ZGC достигает < 1ms pause для heap в 1TB. Это не компромисс - это фундаментальное изменение в том, что стало возможным с managed рантаймами.

  • **LinkedIn** (2022): переход на ZGC для части сервисов снизил p99 latency в 3x при heap 32GB - G1 давал паузы 2-4 секунды при Major GC
  • **Twitter/X** backend: Shenandoah GC для критичных timeline сервисов - паузы < 20ms вместо 200ms+, что устранило visible jitter для пользователей
  • **Go runtime** достигает < 1ms паузы без generational GC через tri-color concurrent marking с точными write barriers - пример что concurrent GC возможен без сложного generational устройства

Concurrent GC

Concurrent GC выполняет большинство работы параллельно с мутатором (работающей программой). Проблема: мутатор изменяет граф объектов во время маркировки. Решение - tri-color invariant: белые (непосещённые), серые (в процессе), чёрные (обработанные). Write barrier следит: если чёрный объект получил ссылку на белый - нужно перекрасить белый в серый.

Go 1.5 (2015) перешёл на полностью concurrent GC с tri-color marking. Go GC pause time: ~0.1-1ms для большинства heap размеров. Go не использует generational GC потому что write barriers для cross-generation references создали бы overhead несовместимый с Go value semantics (стековые переменные копируются, не аллоцируются).

Зачем concurrent GC нужен write barrier при каждой записи указателя?

ZGC: Sub-Millisecond Pauses

ZGC (JDK 15+ production ready) достигает pause time < 1ms для heap до нескольких терабайт. Ключевые техники: colored pointers (метаданные GC в битах указателя), load barriers (проверка при каждом чтении ссылки), concurrent relocation (перемещение объектов без stop-the-world).

ZGC benchmark (2023, heap 128GB): max pause 0.5ms. G1 GC для того же heap: max pause 2-8 секунды. ZGC платит за это ~15% throughput overhead и постоянным background CPU использованием. LinkedIn перевёл часть инфраструктуры на ZGC и снизил tail latency p99 в 3x при работе с объёмными heap.

Как ZGC перемещает объекты concurrent (без stop-the-world) при наличии живых ссылок на них?

Shenandoah GC

Shenandoah (Red Hat, JDK 12+) - concurrent compacting GC с sub-10ms pauses. В отличие от ZGC не использует colored pointers - вместо этого Brooks Pointer (forwarding pointer) в заголовке каждого объекта. Load barrier легче, но объект занимает на 8 байт больше памяти. Shenandoah портирован на OpenJDK 8+ - доступен на старых JVM.

Shenandoah выигрывает у ZGC в latency для малых heap (< 4GB) из-за меньшего overhead load barrier. ZGC выигрывает для больших heap (> 32GB). Обе системы имеют overhead throughput ~10-20% против G1. Red Hat использует Shenandoah в OpenJDK для middleware (Wildfly, Quarkus) где p99 latency критична.

В чём ключевое отличие Brooks Pointer (Shenandoah) от Colored Pointers (ZGC)?

GC Barriers

GC barriers - код, вставляемый JIT-компилятором при каждом чтении (load barrier) или записи (write barrier) ссылки. Разные GC требуют разные barriers. Write barrier нужен всем concurrent и generational GC. Load barrier нужен только concurrent relocating GC (ZGC, Shenandoah). Barriers добавляют 1-15% overhead на производительность.

JIT-компилятор (Turbofan, HotSpot C2) оптимизирует barriers: если анализ доказывает что объект не может быть перемещён (например, null check уже прошёл), barrier может быть удалён. Это называется barrier elision. V8 для WebAssembly linear memory не нуждается в barriers вообще - Wasm память не является managed heap.

Concurrent GC с sub-millisecond pauses решает все проблемы - нужно всегда использовать ZGC

ZGC/Shenandoah тратят 10-20% throughput на barriers и background GC threads; для batch processing G1 или Parallel GC дают лучший overall throughput

Choice of GC - это tradeoff: latency vs throughput vs memory footprint. Kafka broker (throughput-critical) выигрывает от G1/Parallel. API server (latency-critical) выигрывает от ZGC. Benchmark конкретного приложения обязателен

Почему ZGC использует load barrier (при чтении), а не только write barrier (при записи)?

Итоги

  • Concurrent GC использует tri-color marking параллельно с программой; write barriers поддерживают инвариант корректности при мутации графа объектов
  • ZGC достигает < 1ms pause через colored pointers и load barriers: метаданные в битах указателя, concurrent relocation без stop-the-world
  • GC barriers (+1-15% overhead) вставляются JIT-компилятором; barrier elision удаляет ненужные проверки через статический анализ

Связанные темы

Concurrent GC опирается на базовые алгоритмы и взаимодействует с JIT:

  • Основы GC — Concurrent GC строится поверх Mark-Sweep и Copying; generational hypothesis остаётся базой для G1
  • JIT-компиляция основы — JIT вставляет и оптимизирует GC barriers в генерируемый нативный код
  • Управление памятью — Rust borrow checker - альтернатива GC без runtime overhead и без паузы

Вопросы для размышления

  • ZGC достигает < 1ms pause но платит ~15% throughput. В каких production сценариях этот tradeoff оправдан, а в каких - нет?
  • Go не имеет generational GC, но достигает хороших pause times. Что это говорит о применимости generational hypothesis к Go программам?
  • Concurrent GC требует write/load barriers в каждой записи/чтении указателя. Как JIT-компилятор решает, когда barrier можно безопасно удалить?

Связанные уроки

  • plt-29-gc
Продвинутый GC: concurrent и real-time

0

1

Войти