Хабрахабр

На спор: прочитав до конца, вы поймёте, как и почему именно так работает GC

Это глупо и в моем случае — эгоистично. Скажу сразу: я никогда не жду развёрнутого ответа на этот вопрос на собесах. это снимает целый ряд вопросов. Однако, на мой взгляд, помимо общего интереса к платформе, знать, как он работает очень полезно, т.к. Или же если разработчик более опытен, помогает ему автоматически, на уровне мышечной памяти писать код, приводящий к наименьшему количеству проблем. Например, исключает вариант, когда разработчик считает, что Dispose вызывается автоматически и вызывать его самому не надо.

Потому, предлагаю альтернативный подход, описанный в моей книге, . Другой вопрос, что мне субъективно не очень нравится, как объясняется его работа. NET Platform Architecture.

Вместо этого, оставив голову мыслям о полезном, мы просто обоснуем выбор и тем самым поймём, почему выбор был сделан именно таким. Если мы с вами будем досконально разбираться, почему были выбраны именно эти два алгоритма управления памятью: Sweep и Compact, нам для этого придётся рассматривать десятки алгоритмов управления памятью, которые существуют в мире: начиная обычными словарями, заканчивая очень сложными lock-free структурами. Мы более не смотрим в рекламный буклет ракеты-носителя: у нас на руках полный набор документации.

Спор взаимовыгоден: если будет не понятно, я подправлю не ясные моменты в книге, маленькой частью которой является данный текст.

Я выбрал формат рассуждения чтобы вы почувствовали себя архитекторами платформы и сами пришли к тем же самым выводам, к каким пришли реальные архитекторы в штаб-квартире Microsoft в Рэдмонде.

Исходя из классификации выделяемых объектов на основании их размера можно разделить места под выделение памяти на два больших раздела: на место с объектами размером ниже определенного порога и на место с размером выше этого порога и посмотреть, какую разницу можно внести в управление этими группами (исходя из их размера) и что их этого выйдет.

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

В куче нет дополнительных структур данных, которые хранят указатели на каждый объект, которым управляет куча.

А при освобождении памяти нам становится трудно полагаться на линейное прохождение кучи: это долго и не эффективно. Однако, тем не менее, когда память нам более не нужна, мы должны её освобождать. Как следствие, мы приходим к мысли, что надо как-то хранить информацию о свободных участках памяти.

В куче есть списки свободных участков памяти.

Указатель + размер против SyncBlockIndex + VMT + какое-либо поле — в случае объекта). Если, как мы решили, хранить информацию о свободных участках, и при этом при освобождении памяти участки эти оказались слишком малы, то во-первых мы приходим к той-же проблеме хранения информации о свободных участках, с которой столкнулись при рассмотрении занятых (если по бокам от занятых освободился один объект, то чтобы хранить о нём информацию, надо в худшем случае 2/3 его размера. Обычно, они освобождаются в хаотичном порядке. Это снова звучит расточительно, согласитесь: не всегда выпадает удача освобождения группы объектов, следующих друг за другом. А потому возникает вполне естественное желание уменьшить фрагментацию и сжать кучу, переместив все занятые участки на места свободных, образовав тем самым большую зону свободного участка, где можно выделять память. Но в отличии от занятых участков, которые нам нет надобности линейно искать, искать свободные участки нам необходимо потому что при выделении памяти они нам снова могут понадобиться.

Отсюда рождается идея алгоритма Compacting.

Ведь эта операция может быть очень тяжёлой. Но, подождите, скажите вы. И что, скажете вы, надо двигать вообще всё?? Представьте только, что вы освободили объект в самом начале кучи. Но это ведь только начало работы. Ну конечно, можно пофантазировать на тему векторных инструкций CPU, которыми можно воспользоваться для копирования огромного занятого участка памяти. Эта операция может занять дичайше длительное время. Надо ещё исправить все указатели с полей объектов на объекты, которые подверглись передвижениям. Например, разделив весь отрезок памяти кучи на сектора и работать с ними по отдельности. Нет, надо исходить из чего-то другого. Если работать отдельно в каждом секторе (для предсказуемости и масштабирования этой предскамуемости — желательно, фиксированных размеров), идея сжатия уже не кажется такой уж тяжёлой: достаточно сжать отдельно взятый сектор и тогда можно даже начать рассуждать о времени, которое необходимо для сжатия одного такого сектора.

Тут надо обратиться ко второй классификации, которая введена на платформе: разделение памяти, исходя из времени жизни отдельных её элементов. Теперь осталось понять, на основании чего делить на сектора.

Далее, проявив смекалку, можно прийти к выводам, что в приложениях объекты делятся на две группы: те, что создали для долгой жизни и те, которые были созданы жить очень мало. Деление простое: если учесть, что выделять память мы будем по мере возрастания адресов, то первые выделенные объекты становятся самыми старыми, а те, что находятся в старших адресах — самыми молодыми. Или те же DTO объекты. Например, для временного хранения указателей на другие объекты в виде коллекции. Соответственно, время от времени сжимая кучу мы получаем ряд долгоживущих объектов — в младших адресах и ряд короткоживущих — в старших.

Таким образом мы получили поколения.

Разделив память на поколения, мы получаем возможность реже заглядывать в объекты старшего поколения, которых становится всё больше и больше.

Либо мы будем стараться, чтобы GC отрабатывал маскимально быстро: тогда размер младшего поколения мы будем стараться делать минимальных размеров. Но возникает еще один вопрос: если мы будем иметь всего два поколения, мы получим проблемы. Либо, чтобы минимизировать случайное проваливание, мы увеличим размер младшего поколения. Как результат — объекты будут случайно проваливаться в старшее поколение (если GC сработал "прям вот сейчас, во время яростного выделения памяти под множество объектов"). Тогда GC на младшем поколении будет работать достаточно долго, замедляя и подтормаживая приложение.

Подросткового. Выход — введение "среднего" поколения. Суть его введения сводится к получению баланса между получением минимального по размеру младшего поколения и максимально-стабильного старшего поколения, где лучше ничего не трогать. Другими словами, если дожили до подросткового возраста, велика вероятность дожить до старости. Первое (не забываем, что мы считаем с нуля) поколение создается также небольшим и GC туда заглядывает реже. Это — зона, где судьба объектов еще не решена. GC тем самым дает возможность объектам, которые находятся во временном, первом поколении, не уйти в старшее поколение, которое собирать крайне тяжело.

Так мы получили идею трёх поколений.

Ведь если его не делать, мы избавляемся от огромного пласта работы. Следующий слой оптимизации — попытка отказаться от сжатия. Вернемся к вопросу свободных участков.

Тут мы приходим к идее второго алгоритма освобождения памяти в GC, который называется Sweep: память не сжимаем, для размещения новых объектов используем пустоты от освобожденных объектов Если после того, как мы израсходовали всю доступную в куче память и был вызван GC, возникает естественное желание отказаться от сжатия в пользу дальнейшего выделения памяти внутри освободившихся участков, если их размер достаточен для размещения некоторого количества объектов.

Так мы описали и обосновали все основы алгоритмов GC.

Задача данной статьи: описать коротко да так, чтобы были ясны причины выбора архитекторами. Было бы круто, если бы вы написали, что конкретно осталось не ясным.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть