- PVSM.RU - https://www.pvsm.ru -
В данной статье вы встретите сразу два источника информации:
Примечание: чтобы подробнее узнать о сборке мусора в целом см. справочник The Garbage Collection Handbook; специализированная информация о сборщике мусора в CLR приведена в книге Pro .NET Memory Management. Ссылки на оба ресурса даны в конце документа.
Сборка мусора связана с двумя компонентами: распределителем и сборщиком. Распределитель отвечает за выделение памяти и вызов сборщика при необходимости. Сборщик собирает мусор или память объектов, которые больше не используются программой.
Есть и другие способы вызвать сборщик, например вручную, с помощью GC.Collect. Также поток финализатора может получить асинхронное уведомление о том, что память заканчивается (что вызовет сборщик).
Распределитель вызывается вспомогательными компонентами среды выполнения с указанием следующей информации:
Сборщик мусора не предусматривает специальных методов обработки для разных типов объектов. Он получает информацию о размере объекта от среды выполнения.
В зависимости от размера, сборщик делит объекты на две категории: маленькие (< 85 000 байт) и большие (>= 85 000 байт). В целом сборка маленьких и больших объектов может происходить одинаково. Однако сборщик разделяет их по размеру, поскольку сжатие больших объектов требует много ресурсов.
Сборщик мусора выделяет память распределителю с учётом контекстов выделения. Размер контекста выделения определяется блоками выделяемой памяти.
Контексты выделения – небольшие области определённого сегмента кучи, каждая из которых предназначена для определённого потока исполнения. На машине с одним процессором (имеется в виду 1 логический процессор) используется один контекст выделения памяти для объектов поколения 0.
Блок выделяемой памяти – объём памяти, выделяемый распределителем каждый раз, когда ему требуется больше памяти, чтобы расположить объект внутри области. Размер блока обычно составляет 8 Кб, а средний размер управляемых объектов – 35 байт. Поэтому в одном блоке можно расположить множество объектов.
Крупные объекты не используют контексты и блоки. Один большой объект может быть крупнее, чем эти маленькие участки памяти. Кроме того, преимущества использования этих участков (описано ниже) проявляются только при работе с маленькими объектами. Пространство для больших объектов выделяется напрямую в сегменте кучи.
Распределитель устроен таким образом, чтобы:
вызывать сборщик мусора, когда необходимо: распределитель вызывает сборщик, когда объём памяти, выделенной для объектов, превышает пороговое значение (установленное сборщиком), или если распределитель больше не может выделять память в данном сегменте. Пороговые значения и управляемые сегменты будут подробно описаны далее.
сохранить местоположение объектов: объекты, находящиеся вместе в одном сегменте кучи, хранятся по виртуальным адресам близким друг к другу.
эффективно использовать кэш: распределитель выделяет память блоками, а не для каждого объекта. Он обнуляет так много памяти, чтобы подготовить кэш процессора, поскольку некоторые объекты будут размещены прямо в нём. Блок выделяемой памяти обычно равен 8 Кб.
эффективно ограничивать область, выделенную потоку исполнения: близость контекстов и блоков памяти, выделенных для потока, гарантирует, что только один поток будет записывать данные в выделенный участок пространства. В результате не нужно ограничивать выделение памяти, пока пространство в текущем контексте выделения ещё не закончилось.
обеспечивать целостность памяти: сборщик мусора всегда обнуляет память для вновь размещаемых объектов, чтобы их ссылки не указывали на произвольные участки памяти.
обеспечивать непрерывность кучи: распределитель создаёт свободный объект из оставшейся памяти в каждом выделенном блоке. Например, если в блоке осталось 30 байт, а для размещения следующего объекта необходимо 40 байт, распределитель превратит эти 30 байт в свободный объект и запросит новый блок памяти.
Object* GCHeap::Alloc(size_t size, флаги DWORD);
Object* GCHeap::Alloc(alloc_context* acontext, size_t size, флаги DWORD);
С помощью указанных функций можно выделять память как для маленьких, так и больших объектов. Существует функция для выделения места прямо в куче больших объектов (LOH):
Object* GCHeap::AllocLHeap(size_t size, флаги DWORD);
GC предназначен для эффективного управления памятью. Разработчики, которые пишут управляемый код, смогут использовать его без особых усилий. Эффективное управление означает что:
Сборщик мусора CLR собирает объекты, логически разделённые по поколениям. После сборки объектов в поколении N, оставшиеся объекты маркируются как принадлежащие поколению N+1. Этот процесс называется продвижение объектов по поколениям. В этом процессе существуют исключения, когда необходимо перевести объект в поколение ниже или не продвигать его вообще.
В случае маленьких объектов куча делится на три поколения: gen0, gen1 и gen2. Для больших объектов существует только одно поколение – gen3. Gen0 и gen1 называют эфемерными поколениями (объекты живут в них короткое время).
Для кучи небольших объектов номер поколения означает их возраст. Например, gen0 – самое молодое поколение. Это не значит, что все объекты в gen0 моложе объектов в gen1 или gen2. Здесь существуют исключения, которые описаны ниже. Сборка поколения означает сборку объектов в этом поколении, а также во всех его более молодых поколениях.
Теоретически сборка больших и маленьких объектов может происходить одинаково. Однако поскольку сжатие крупных объектов требует много ресурсов, их сборка происходит по-другому. Большие объекты содержатся только в gen2 и собираются только во время сборки мусора в этом поколении из соображений производительности. Как gen2, так и gen3 могут быть большими, а сборка объекта в эфемерных поколениях (gen0 и gen1) не должна быть слишком ресурсозатратной.
Объекты размещаются в самом младшем поколении. Для маленьких объектов это gen0, а для больших – gen3.
Управляемая куча состоит из набора сегментов. Сегмент – непрерывный блок памяти, который ОС передаёт сборщику мусора. Сегменты кучи разбиваются на мелкие и большие участки для размещения маленьких и больших объектов. Сегменты каждой кучи связаны вместе. По крайней мере один сегмент для маленького объекта и один для большого резервируются при загрузке CLR.
В каждой куче маленьких объектов есть только один эфемерный сегмент, где находятся поколения gen0 и gen1. Этот сегмент может содержать или не содержать объекты поколения gen2. Кроме эфемерных сегментов, могут существовать один или более дополнительных сегментов, которые будут сегментами gen2, так как они содержат объекты поколения 2.
Куча больших объектов состоит из одного или более сегментов.
Сегмент кучи заполняется от младших адресов к старшим. Это означает, что объекты, расположенные по младшим адресам сегмента, старше чем те, что находятся по старшим. Здесь также есть исключения, которые описаны ниже.
Сегменты кучи выделяются по мере необходимости. Если они не содержат используемых объектов, сегменты удаляются. Однако начальный сегмент в куче всегда существует. За один раз для каждой кучи выделяется один сегмент. В случае маленьких объектов это происходит во время сборки мусора, а для больших – во время выделения памяти для них. Такая схема повышает производительность, поскольку большие объекты собираются только в поколении 2 (что требует много ресурсов).
Сегменты кучи соединены вместе в порядке выделения. Последний сегмент в цепочке всегда является эфемерным. Сегменты, в которых собраны все объекты, можно использовать заново, например в качестве эфемерных. Повторное использование сегментов применяется только для кучи маленьких объектов. Для размещения больших объектов каждый раз рассматривается вся куча больших объектов. Маленькие объекты размещаются только в эфемерных сегментах.
Это логическое понятие, связанное с размером каждого поколения. Если он превышен, в поколении начинается сборка мусора.
Пороговое значение для определённого поколения устанавливается в зависимости от количества выживающих в этом поколении объектов. Если это количество высоко, пороговое значение становится выше. При этом ожидается, что соотношение используемых и неиспользуемых объектов будет лучше во время следующей сессии сборки мусора в этом поколении.
При активации сборщик должен определить, в каком поколении проводить сборку. Кроме порогового значения, на этот выбор влияют и другие факторы:
Во время этого этапа CLR должна найти все живые объекты.
Преимущество сборщика с поддержкой поколений заключается в его способности убирать мусор только в части кучи, вместо того, чтобы всё время наблюдать за всеми объектами. Собирая мусор в эфемерных поколениях, сборщик должен получить у среды исполнения информацию о том, какие объекты в этих поколениях по-прежнему используются программой. Кроме того, объекты в старших поколениях могут использовать объекты в младших поколениях, ссылаясь на них.
Чтобы пометить старые объекты, ссылающиеся на новые, сборщик мусора использует специальные биты. Биты устанавливаются механизмом JIT-компилятора во время операций присвоения. Если объект принадлежит к эфемерному поколению, JIT-компилятор установит байт, содержащий бит, указывающий на исходное положение. Собирая мусор в эфемерных поколениях, сборщик может использовать эти биты для всей оставшейся кучи и просмотреть только те объекты, которым эти биты соответствуют.
На этом этапе моделируется сжатие, чтобы определить его эффективность. Если результат оказывается продуктивным, сборщик начинает фактическое сжатие. В противном случае он просто производит уборку.
Если сборщик выполняет сжатие, это приведёт к перемещению объектов. В этом случае необходимо обновить ссылки на эти объекты. Во время этапа перемещения сборщик должен найти все ссылки, указывающие на объекты в тех поколениях, где проводится сборка мусора. Напротив, во время стадии маркировки сборщик помечает только живые объекты, поэтому ему не нужно рассматривать слабые ссылки.
Этот этап достаточно прост, поскольку сборщик уже определил новые адреса для перемещения объектов во время этапа планирования. При сжатии объекты будут скопированы по этим адресам.
Во время этого этапа сборщик ищет неиспользуемое пространство между живыми объектами. Вместо этого пространства он создаёт свободные объекты. Неиспользуемые объекты, находящиеся рядом, становятся одним свободным объектом. Все свободные объекты помещаются в список свободных объектов.
Термины:
SuspendEE
, чтобы приостановить все управляемые потоки.RestartEE
, чтобы вновь запустить управляемые потоки.Этот алгоритм описывает фоновую сборку мусора.
SuspendEE
, чтобы приостановить все управляемые потоки.RestartEE
, чтобы возобновить управляемые потоки.SuspendEE
, чтобы завершить маркировку, а затем вызывает RestartEE
, чтобы запустить параллельную уборку с работающими пользовательскими потоками.SuspendEE
, чтобы приостановить выполнение управляемых потоков.RestartEE
, чтобы запустить управляемые потоки.Алгоритм такой же, как и в случае с параллельной сборкой мусора в режиме рабочей станции, только нефоновая сборка выполняется в серверных потоках.
Эта секция поможет понять code flow.
Когда у пользовательского потока заканчивается память, он может получить свободное пространство с помощью функции try_allocate_more_space
.
Функция try_allocate_more_space
вызывает GarbageCollectGeneration
, когда нужно запустить сборщик мусора.
Если сборка мусора в режиме рабочей станции производится непараллельно, GarbageCollectGeneration
выполняется в пользовательском потоке, который вызвал сборщик мусора. Поток кода выглядит следующим образом:
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
}
garbage_collect()
{
generation_to_condemn();
gc1();
}
gc1()
{
mark_phase();
plan_phase();
}
plan_phase()
{
// фактический этап планирования, чтоб принять
// решение о сжатии
if (compact)
{
relocate_phase();
compact_phase();
}
else
make_free_lists();
}
Если выполняется параллельная сборка мусора в режиме рабочей станции (по умолчанию), поток кода для фоновой сборки мусора выглядит так:
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
}
garbage_collect()
{
generation_to_condemn();
// решение провести фоновую сборку
// активация потока фоновой сборки мусора
do_background_gc();
}
do_background_gc()
{
init_background_gc();
start_c_gc ();
// подождать пока фоновый процесс сборки мусора не перезапустить работу управляемых потоков.
wait_to_proceed();
}
bgc_thread_function()
{
while (1)
{
// подождать возникновения события
// активировать
gc1();
}
}
gc1()
{
background_mark_phase();
background_sweep();
}
Автор: sidristij
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/330397
Ссылки в тексте:
[1] текущий семинар здесь: http://clrium.ru/
[2] Image: https://www.youtube.com/watch?v=DVnmGW6964o&list=PLBwwJL9lzKMY3At-QQQijfiHdsYnOQ7vY
[3] @maoni0: https://github.com/maoni0
[4] .NET CLR GC Implementation: https://raw.githubusercontent.com/dotnet/coreclr/master/src/gc/gc.cpp
[5] The Garbage Collection Handbook: The Art of Automatic Memory Management: http://www.amazon.com/Garbage-Collection-Handbook-Management-Algorithms/dp/1420082795
[6] Garbage collection (Wikipedia): http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)
[7] Pro .NET Memory Management: https://prodotnetmemory.com/
[8] Источник: https://habr.com/ru/post/463293/?utm_campaign=463293&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.