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

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

Основы GC: за что рантайм берёт налог

Суть Сборщик мусора автоматически освобождает память в куче — но работа проявляется как паузы и CPU. Именно rate аллокаций, а не размер кучи, определяет хвостовую latency.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 12 min

p99 latency сервиса взлетает до 800 мс каждые несколько секунд. CPU в норме. Медленных запросов нет. Открываем GC-лог — паузы 600-700 мс каждые 4 секунды. Сервис аллоцирует 1 ГБ/сек, и рантайм останавливает весь мир для уборки.

Что делает сборщик мусора

Сборщик мусора обменивает CPU на memory safety: обходит кучу, маркирует всё достижимое от корней (регистры, стеки потоков, глобальные переменные) и возвращает остальное. Вам не нужно вызывать free() — рантайм делает это за вас. Цена: рантайм тратит CPU на учёт, и иногда останавливает приложение для этого.

Три рычага всегда в напряжении:

  • Время паузы — как долго приложение остановлено, пока GC работает.
  • Throughput — сколько CPU коллектор забирает у вашего кода.
  • Размер кучи — сколько overhead-памяти нужно коллектору для эффективной работы.

Минимизировать все три одновременно невозможно.

Метафора кухни

Представьте кухню ресторана. Повара (ваш код) производят грязную посуду (аллокации). Посудомойщик (GC) её чистит. Если повара генерят посуду быстрее, чем мойщик успевает, посуда копится — кухне приходится останавливаться, чтобы разгрузить раковину. Современный concurrent-посудомойщик моет, пока повара работают; старый stop-the-world-посудомойщик заставляет всех ждать. Даже лучший посудомойщик стоит воды и электричества (CPU). Быстрая кухня — не от магического посудомойщика: от поваров, переиспользующих тарелки.

Почему rate аллокаций важнее размера кучи

Размер кучи показывает, сколько памяти программа держит в данный момент. Rate аллокаций показывает, как часто запрашивается новая память. Циклы GC запускаются пропорционально скорости накопления мусора — то есть rate аллокаций, а не размер кучи.

СценарийРазмер кучиRate аллокацийЧастота пауз GC
Большой стабильный кэш4 ГБ50 МБ/сРаз в ~80 с, короткие паузы
High-throughput API100 МБ1 ГБ/сКаждые ~0,1 с, частые спайки

Сервис с 4 ГБ кучи, но медленными аллокациями, почти не замечает пауз. Сервис со 100 МБ кучи и rate 1 ГБ/с — в постоянном GC. Рычаг для хвостовой latency — rate аллокаций, а не размер кучи.

Цикл mark-sweep

Любой tracing GC следует одной схеме:

  1. Сканирование корней — кратко останавливаем, определяем корни (регистры, стеки, глобалы).
  2. Маркировка — обходим граф ссылок от корней; маркируем каждый достижимый объект.
  3. Очистка — возвращаем память каждого немаркированного объекта.
  4. Опционально компактизация — перемещаем живые объекты, устраняя фрагментацию.
  5. Обновление ссылок — исправляем указатели на перемещённые объекты.
  6. Возобновление — приложение работает на полной скорости до следующего цикла.

Наивная версия паузит приложение на шагах 1–5. Для кучи 32 ГБ это могут быть секунды — неприемлемо для latency-sensitive сервиса. Современные коллекторы сокращают или устраняют большинство STW-фаз.

Concurrent vs stop-the-world GC

Stop-the-world (STW) коллектор паузит все потоки приложения, пока работает. Простой в реализации; паузы растут с размером кучи.

Concurrent коллектор выполняет большую часть работы параллельно с потоками приложения, поэтому видимые пользователю паузы коротки (субмиллисекундные) вместо длинных (десятки-сотни мс). Ему нужны write barriers — небольшие фрагменты кода, выполняемые при каждой записи ссылки, чтобы держать коллектор в курсе. Стоимость барьера ~2-10% CPU; выгода — короткие паузы.

Почему это работает

Все современные concurrent GC всё ещё имеют некоторые stop-the-world фазы — сканирование корней, обработка weak-ref, remap. Concurrent коллекторы минимизируют STW, но не устраняют его. Всегда смотрите GC-логи на реальное распределение пауз, а не на заголовки от вендоров.

Викторина

p99 latency сервиса коррелирует с паузами GC. Что проверить ПЕРВЫМ?

Викторина

Почему concurrent GC предпочтительнее stop-the-world GC для production-сервисов?

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

Расставьте концептуальные стадии типичного цикла сборки мусора по порядку:

  1. 1 Определить корни — регистры, стеки потоков, глобальные переменные
  2. 2 Маркировка — обход от корней, пометка каждого достижимого объекта
  3. 3 Очистка — возврат памяти немаркированных объектов
  4. 4 Опционально компактизация — перемещение живых объектов для устранения фрагментации
  5. 5 Обновление ссылок на перемещённые объекты
  6. 6 Возобновление приложения на полной скорости до следующего цикла
Закончи аналогию

Заполните пропуск: сборка мусора обменивает CPU на memory _______ — вам не нужно отслеживать каждую аллокацию вручную, но рантайм платит за учёт циклами, которые могли бы достаться вашему коду.

Вспомните перед уходом
  1. 01
    В одном абзаце: почему rate аллокаций важнее общего размера кучи для хвостовой latency, определяемой GC?
  2. 02
    Назовите 3-way tradeoff любого GC и приведите пример коллектора, оптимизирующего каждый из крайних значений.
Итог

Сборщик мусора маркирует достижимые объекты и возвращает остальное, обменивая циклы CPU на memory safety. Три рычага — время паузы, throughput и размер кучи — нельзя минимизировать одновременно. Rate аллокаций определяет частоту циклов GC сильнее, чем размер кучи: небольшой сервис с rate 1 ГБ/с испытывает куда большее давление GC, чем большой кэш с rate 50 МБ/с. Современные concurrent GC делают большую часть работы параллельно с потоками приложения, удерживая паузы ниже 1 мс, но всё ещё имеют короткие STW-фазы для сканирования корней. Первый рычаг для хвостовой latency от GC — всегда профиль аллокаций, а не выбор коллектора.

Связанные уроки
встречается в159
Продолжить восхождение ↑Алгоритмы GC: поколенческая гипотеза, concurrent marking и write barrier
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.