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

Браузер и фронтенд-рантайм

Orinoco GC: параллельный scavenger, конкурентная разметка и барьеры записи

Суть Как сборщик мусора Orinoco в V8 разделяет молодое и старое поколения, запускает малый GC параллельно, перемещает разметку крупного GC на фоновый поток и использует барьеры записи для корректности при выполнении JS.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Node.js-сервис нормально работает неделю, затем каждые 60 секунд появляется 5мс пауза в латентности p99. Код приложения не менялся. Цикл GC пересёк порог старого поколения. Понимание Orinoco — это разница между угадыванием и диагностикой.

Структура кучи

Куча V8 разделена на два поколения:

  • Молодое поколение — только что выделенные объекты, обычно 1–8 МБ. Собирается часто (малый GC).
  • Старое поколение — долгоживущие объекты, может быть сотни МБ. Собирается редко (крупный GC).

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

Малый GC: параллельный Scavenger

Scavenger использует копирующий сборщик с двумя полупространствами:

  1. From-space содержит живые объекты; to-space пусто.
  2. При сборке V8 обходит корни (стек, глобальные, регистры), копирует достижимые молодые объекты в to-space, обновляет указатели.
  3. Мёртвые объекты брошены в from-space.
  4. Пережившие два малых GC продвигаются в старое поколение.

Scavenger параллелен с первой фазы Orinoco (V8 6.2, 2017): несколько рабочих потоков делят работу через динамическое воровство задач (work-stealing). Паузы основного потока ~1мс, поскольку живое множество молодого поколения мало, а параллелизм держит wall-clock время низким.

Крупный GC: Mark-Compact с конкурентной разметкой

Сборка старого поколения трассирует все живые объекты, подметает мёртвые и уплотняет выжившие для снижения фрагментации. До Orinoco это была операция stop-the-world длительностью сотни мс на больших кучах.

Ключевое нововведение Orinoco: конкурентная разметка — фоновый поток обходит кучу, пока JavaScript выполняется на основном потоке. Основной поток платит краткую финальную паузу разметки (однозначные мс) плюс фазу подметания/уплотнения (параллельная по рабочим потокам, но всё ещё блокирующая). Результат: пауза основного потока снижена ~на 50% на WebGL-нагрузках.

Числа Orinoco GC
Пауза малого GC (Scavenger)
<1 мс типично
Снижение паузы конкурентной разметки
~50% на WebGL
Параллельный Scavenger отгружен
V8 6.2 (2017)
Размер кучи молодого поколения
1–8 МБ
Порог продвижения
пережил 2 малых GC
Цель паузы GC при 60fps
<16.6 мс на кадр

Барьеры записи

Чтобы конкурентная разметка была корректной, пока JS одновременно мутирует кучу, V8 использует барьеры записи: небольшой код, генерируемый при каждой записи свойства, которая может пересечь от чёрного (уже помеченного) объекта к белому (ещё не посещённому).

V8 использует семантику snapshot-at-the-beginning (Юаса): когда основной поток перезаписывает ссылку на белый объект, барьер затемняет исходный (перезаписанный) референт в серый, чтобы маркер всё равно его посетил. Это сохраняет инвариант «каждый объект, достижимый при начале разметки, будет посещён», даже если основной поток опережает. Барьер записи стоит ~3–5 тактов на запись; V8 значительно вкладывает в то, чтобы держать его дешёвым.

Современный V8 использует три-цветную разметку (белый/серый/чёрный) с семантикой snapshot-at-the-beginning и гибридным конкурентным + инкрементным планированием. Инкрементная разметка амортизирует работу GC по выполнению JS без наблюдаемой единой паузы.

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

Упорядочите шаги параллельного малого GC (Scavenger):

  1. 1 Аллокация переполняет кучу молодого поколения
  2. 2 Основной поток инициирует Scavenger, рабочие потоки присоединяются
  3. 3 Обходим корни: стек, глобальные, регистры — находим живые молодые объекты
  4. 4 Копируем живые объекты из from-space в to-space через work-stealing
  5. 5 Обновляем указатели по всей куче на новые расположения
  6. 6 Объекты, пережившие два GC-цикла, продвигаются в старое поколение
  7. 7 From-space объявляется пустым, to-space становится новым from-space
Проследи
1/5

Node.js-процесс потребляет 4ГБ RAM через неделю работы. Трасируйте паттерн GC.

1
Step 1 of 5
Шаг 1: 4ГБ через неделю — медленная утечка, не краш. Какие V8-наблюдения покажут тренд?
2
Locked
Шаг 2: heap-after стабильно растёт на 50МБ/день. Какой вид утечки?
3
Locked
Шаг 3: как найти источник утечки?
4
Locked
Шаг 4: утечка найдена — LRU-кэш без максимального размера. Исправление?
5
Locked
Шаг 5: предотвратить повторение?
Викторина

Параллельный scavenger Orinoco снижает паузы малого GC через work-stealing. Почему крупный GC Mark-Compact всё ещё сложнее?

Викторина

Почему конкурентная разметка нуждается в барьерах записи?

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

Зачем вообще нужно деление на поколения? Большинство выделяемых объектов умирают быстро — временный буфер, промис, React-элемент на каждый рендер. Собирать только молодое поколение (которое мало) намного дешевле, чем сканировать всю кучу. Старое поколение нуждается в сборке только редко. Эта «генерационная гипотеза» эмпирически верна для большинства программ и является причиной, по которой генерационные GC стали отраслевым стандартом в 1990-х годах.

Вспомните перед уходом
  1. 01
    Опишите цикл малого GC (Scavenger) в V8.
  2. 02
    Как конкурентная разметка в Orinoco не допускает повреждения кучи?
  3. 03
    Какова самая частая причина медленной утечки памяти в Node.js и как её найти?
Итог

Orinoco — генерационный сборщик мусора V8. Новые объекты попадают в молодое поколение (~1–8 МБ); пережившие два малых GC-цикла продвигаются в старое поколение. Малый GC использует копирующий scavenger, работающий параллельно на рабочих потоках — паузы обычно менее 1мс. Крупный GC (старое поколение) использует mark-compact; конкурентная разметка перемещает обход графа объектов на фоновый поток, пока выполняется JS, снижая паузы основного потока ~на 50% на WebGL-нагрузках. Барьеры записи при каждой записи свойства держат конкурентный маркер корректным при изменении JS-потоком ссылок в середине цикла. Инкрементная разметка дополнительно амортизирует работу по выполнению JS. Фундаментальный компромисс: 3–5 тактов на запись (накладные расходы барьера) в обмен на устранение стоп-мир пауз в сотни мс, бывших нормой до Orinoco.

Связанные уроки
встречается в143
Продолжить восхождение ↑Спекулятивный движок TurboFan и ловушка deopt-loop
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.