Сборщик мусора в Dart. Часть 3: старое поколение - QubStore

Сборщик мусора в Dart. Часть 3: старое поколение

В предыдущих материалах мы разобрали, как виртуальная машина Dart классифицирует объекты и проводит зачистку среди «молодёжи». Если коротко напомнить суть второго этапа: все выжившие объекты переезжали в новое полупространство, а старая область памяти просто признавалась мусором. Но по отношению к «старикам» этот номер не работает — и сейчас выясним, почему.

Сборщик мусора в Dart. Часть 3: старое поколение
Сборщик мусора в Dart. Часть 3: старое поколение
Сборщик мусора в Dart. Часть 3: старое поколение
Сборщик мусора в Dart. Часть 3: старое поколение
Сборщик мусора в Dart. Часть 3: старое поколение

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

На первом этапе (mark) сборщик обходит все достижимые объекты от корней — а это локальные переменные в работающих функциях, глобалы, статики и внутренние ссылки Dart VM. У каждого объекта есть служебный бит пометки, который в начале зачистки сброшен. Дойдя до живого объекта, сборщик взводит этот бит. После обхода картина ясна: у кого стоит бит — тот жив. На втором этапе (sweep) сканер проходит по всей памяти и проверяет эти флаги. Если бит не установлен — объект мертв, и его место возвращается в список свободных блоков (free list), куда потом будут помещаться новые объекты. У «живых» же бит сразу сбрасывается в ноль, чтобы подготовиться к следующему циклу. Отдельный приятный бонус: если страница — а Dart запрашивает память у ОС крупными кусками фиксированного размера — заполнена только мусором, она целиком освобождается и отдаётся обратно системе.

Но после очистки память станет пористой, как швейцарский сыр: «живые» останутся стоять на месте, а между ними зазияют пустоты от удалённых. Свободный объём может быть велик, однако он оказывается нашинкован на разные по размеру ломтики. Запроси программа память под крупный объект — и окажется, что единого куска нужной величины нет. Это явление называется фрагментацией памяти и с ним нужно бороться. Dart делает это за счёт второго алгоритма — mark-compact. После фазы «mark» выжившие объекты сдвигаются в плотную группу, в начало памяти, сохраняя свой исходный порядок (так называемый скользящий уплотнитель). И тогда всё свободное место сливается в один большой сплошной массив. Уплотнение запускается только тогда, когда фрагментация действительно создаёт неудобства. Логичная цена за это — переезд объектов и изменение их адресов, поэтому требуется механизм обновления ссылок.

Для переадресации Dart использует таблицу пересылки (forwarding table). Вся куча разделена на страницы фиксированного размера, и каждая начинается с круглого адреса — чтобы добраться до нужного заголовка, достаточно просто обнулить младшие биты. Таблица для каждого блока хранит его новый адрес и битовый вектор. Как высчитать новый адрес конкретного объекта? Взять адрес блока, добавить смещение на количество живых ячеек до объекта — и всё готово.

Замечаете заметные паузы в работе приложения, глядя на DevTools? В большинстве случаев виновата именно очистка старшего поколения — за её ходом можно проследить во вкладке Memory. Почему так? Из-за трёх тяжёлых шагов: полный проход по всем живым объектам, ещё одно сканирование памяти при зачистке и — при необходимости — сдвиг всех жителей с исправлением всех ссылок. Всё это заметно дольше, чем в молодом поколении. Отсюда прямой вывод: чем больше у вас долгоживущих объектов в стиле крупных кэшей, разрастающегося состояния или раздутых коллекций, тем более затратной становится их уборка. За такими структурами нужно следить и учитывать нюансы старого поколения. Но почему всё это не превращается в долгие фризы? Ответ — в финальной, четвёртой части, где мы посмотрим на Concurrent Marking, барьеры записи и финализаторы.

Ссылка на основную публикацию