open atlas
↑ К треку
Внутренности движка JavaScript JSE · 06 · 03

Major GC: mark, sweep, compact

Old space собирается через mark-sweep-compact. Разметка использует трёхцветную абстракцию (white/grey/black) и идёт инкрементально и конкурентно, чтобы сжать окно stop-the-world; sweep освобождает белые объекты в free lists; compaction эвакуирует живые объекты

JSE Senior ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior

Scavenger субмиллисекундный, потому что молодое пространство крошечное. Old space — противоположность: это может быть гигабайт долгоживущих объектов, и вы не можете «выкопироваться» из него, как Scavenger, — нет запасного гигабайта, куда копировать. Поэтому major GC принимает другую форму, и именно его пауза ваши пользователи ощущают как пропущенный кадр или зависший запрос. Вся инженерная история современного major GC — это одно предложение: делать как можно больше работы вне главного потока, чтобы часть, которая обязана остановить мир, сжималась к нулю.

Почему old space нужен другой сборщик

Копирующему Scavenger’у нужна вторая область размером с живое множество, куда копировать; это нормально для нескольких мегабайт молодых объектов, но невозможно для old generation в гигабайт. Поэтому old space использует mark-sweep-compact (MSC): найти живые объекты на месте (mark), освободить мёртвые на месте (sweep) и лишь изредка переместить выживших, чтобы бороться с фрагментацией (compact). Major GC трассирует весь достижимый граф, а не маленькую область, поэтому его стоимость принципиально больше — и поэтому V8 столь усиленно вкладывается в сокрытие этой стоимости.

Разметка: трёхцветная абстракция

Разметка — это обход графа с приёмом, делающим его паузируемым. Каждый объект концептуально один из трёх цветов (хранится в двух mark-битах):

  • white (белый) — ещё не достигнут (предполагается мёртвым);
  • grey (серый) — достигнут, но его исходящие ссылки ещё не просканированы (в рабочей очереди / «marking worklist»);
  • black (чёрный) — достигнут и полностью просканирован (все его ссылки уже посерены/обработаны).

Алгоритм:

  1. Покрасить всё в white. Покрасить прямых референтов корней в grey (положить их в worklist).
  2. Извлечь grey-объект, просканировать его ссылки — покрасить каждого белого референта, на которого он указывает, в grey — затем покрасить сам объект в black.
  3. Повторять, пока не останется grey-объектов.
  4. Каждый всё ещё white объект недостижим → мусор.

Серое множество — это фронт волны, идущей по графу; когда оно истощается, трассировка завершена. Ключевой инвариант, который это поддерживает: чёрный объект никогда не должен указывать напрямую на белый («трёхцветный инвариант»). Если бы это требовалось только для stop-the-world трассировки, было бы тривиально, — но V8 не останавливает мир для разметки, и вот тут становится интересно.

Инкрементальная и конкурентная разметка

Наивный MSC останавливает весь JavaScript, размечает всю кучу, затем возобновляет — пауза stop-the-world в сотни миллисекунд на большой куче. V8 атакует это с двух сторон:

  • Incremental marking разбивает трассировку на маленькие шаги, перемежающиеся с исполнением JS. Движок размечает чуть-чуть, запускает JS, размечает ещё чуть-чуть — амортизируя работу, чтобы ни одна пауза не была длинной. Шаги планируются в простой и во время аллокации.
  • Concurrent marking идёт дальше: фоновые вспомогательные потоки обходят граф объектов пока главный поток продолжает исполнять JavaScript. Основная масса работы разметки происходит полностью вне главного потока.

Оба создают одну и ту же опасность: главный поток может изменить граф объектов посреди трассировки — записать указатель, которого маркер не видел, — и нарушить трёхцветный инвариант, заставив чёрный объект указать на белый. Тогда маркер завершится, увидит этот белый объект как недостигнутый и ошибочно соберёт живой объект. Решение — write barrier, целиком предмет следующего урока: он перехватывает записи указателей во время разметки и пересеривает затронутый объект, чтобы инвариант держался. Держите эту нить; здесь просто зафиксируйте, что инкрементальная/конкурентная разметка корректна только благодаря write barriers.

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

Sweep: освобождаем white в free lists

Когда разметка закончена, каждый белый объект мёртв. Sweep обходит страницы old space и добавляет память, занятую белыми объектами, в free lists по классам размеров — учёт, позволяющий будущим аллокациям old space найти дыру нужного размера. Sweep ничего не двигает; он лишь записывает свободные области. V8 делает sweep конкурентно и лениво: free lists перестраиваются в фоновых потоках, а страница может быть подметена по требованию при первой аллокации, которой нужно место из неё. Поскольку sweep касается мёртвых объектов (на которые нет ссылок), он естественно распараллеливается и в основном вне критического пути.

Compaction: эвакуация для дефрагментации

Один mark-sweep оставляет фрагментацию: живые и мёртвые объекты перемешаны, поэтому со временем old space становится швейцарским сыром — свободных байтов масса, но нет непрерывной дыры, достаточной для большой аллокации. Compaction чинит это эвакуацией (evacuation): выбрать самые фрагментированные страницы, скопировать их живые объекты на свежую плотно упакованную страницу и обновить каждый указатель, ссылавшийся на перемещённые объекты. Это та же машинерия copy-and-forward, что у Scavenger’а, применённая выборочно. Compaction дорогой (он двигает объекты и переписывает указатели), поэтому V8 делает его выборочно — только на страницах, чья фрагментация это оправдывает, а не на всей куче каждый цикл.

Анатомия major GC
Размер old space
сотни МБ — гигабайты
Пауза STW до инкрементальности
сотни мс
Финальная пауза разметки (совр.)
единицы мс
Цвета разметки
white / grey / black
Разметка идёт
incremental + concurrent
Sweep / compact
конкурентно / выборочно
Викторина

Разметка завершена, а объект всё ещё white. Что это значит и что с ним произойдёт?

Викторина

Concurrent marking делает большую часть работы в фоновых потоках, пока JS исполняется. Почему всё ещё есть короткая финальная пауза stop-the-world?

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

Расставьте по порядку современный цикл major GC в V8.

  1. 1 Покрасить референтов корней в grey, всё остальное в white
  2. 2 Конкурентно/инкрементально извлекать grey-объекты, серить их белых референтов, чернить их
  3. 3 Короткая атомарная пауза: пересканировать корни и помеченные барьером объекты, истощить последнее grey
  4. 4 Sweep: записать white (мёртвые) объекты в free lists, конкурентно и лениво
  5. 5 Compact: выборочно эвакуировать живые объекты с фрагментированных страниц, обновляя указатели
Почему это работает

Зачем вообще делать compaction, а не просто sweep вечно? Потому что долгоживущий сервер аллоцирует и освобождает объекты многих размеров, и аллокация по free-list не всегда может переиспользовать освобождённую дыру — объект на 200 байт не влезет в зазор на 64 байта. За дни old space накапливает непригодные зазоры; резидентная память остаётся высокой, хотя живых данных мало, и большие аллокации начинают падать или вынуждают рост. Выборочный compaction возвращает эту утраченную непрерывность. Это GC-аналог дефрагментации диска.

Вспомните перед уходом
  1. 01
    Объясните трёхцветную абстракцию разметки и что значит «разметка завершена».
  2. 02
    Как V8 держит паузы major GC короткими и какова остаточная стоимость stop-the-world?
  3. 03
    В чём разница между sweep и compaction и когда V8 делает compaction?
Итог

Old space нельзя собирать копированием, как молодое (нет запасного гигабайта, куда копировать), поэтому V8 использует mark-sweep-compact. Разметка использует трёхцветную абстракцию: объекты white (недостигнут), grey (достигнут, но не просканирован, в worklist) или black (достигнут и полностью просканирован); трассировка серит референтов корней, повторно чернит grey-объекты, серя их белых референтов, и кончается, когда не осталось grey, — оставляя каждый всё ещё белый объект мусором. Чтобы держать паузы короткими, V8 размечает инкрементально (маленькие шаги вперемешку с JS) и конкурентно (фоновые потоки размечают, пока JS работает), сводя часть stop-the-world к финальной паузе в единицы миллисекунд, которая согласует мутации через write barrier и истощает последнее серое множество. Затем sweep записывает мёртвые объекты в free lists, ничего не двигая, конкурентно и лениво. Compaction — отдельная выборочная фаза, эвакуирующая живые объекты с самых фрагментированных страниц и переписывающая их указатели, чтобы вернуть непрерывное пространство. Теперь, когда вы видите паузу major GC в трейсе, вы читаете её как финальный шаг в единицы миллисекунд — не сотни миллисекунд наивного stop-the-world MSC, — зная, что основная работа уже шла в фоновом потоке, пока ваш код работал.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки
встречается в208

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.