Хабрахабр

Garbage Collector. Полный курс + перевод из BOTR

В данной статье вы встретите сразу два источника информации:

  1. Полный курс по работе Garbage Collector на русском языке: CLRium #6 (текущий семинар здесь)
  2. Перевод статьи из BOTR "Устройство сборщика мусора" от Маони Стевенс.

1. CLRium #5: Полный курс по Garbage Collector

2. Устройство сборщика мусора by Maoni Stephens (@maoni0)

справочник The Garbage Collection Handbook; специализированная информация о сборщике мусора в CLR приведена в книге Pro . Примечание: чтобы подробнее узнать о сборке мусора в целом см. Ссылки на оба ресурса даны в конце документа. NET Memory Management.

Архитектура компонентов

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

Collect. Есть и другие способы вызвать сборщик, например вручную, с помощью GC. Также поток финализатора может получить асинхронное уведомление о том, что память заканчивается (что вызовет сборщик).

Устройство распределителя

Распределитель вызывается вспомогательными компонентами среды выполнения с указанием следующей информации:

  • необходимый размер выделяемого участка;
  • контекст выделения памяти для потока исполнения;
  • флаги, которые указывают, например, является ли объект финализируемым.

Он получает информацию о размере объекта от среды выполнения. Сборщик мусора не предусматривает специальных методов обработки для разных типов объектов.

В целом сборка маленьких и больших объектов может происходить одинаково. В зависимости от размера, сборщик делит объекты на две категории: маленькие (< 85 000 байт) и большие (>= 85 000 байт). Однако сборщик разделяет их по размеру, поскольку сжатие больших объектов требует много ресурсов.

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

  • На машине с одним процессором (имеется в виду 1 логический процессор) используется один контекст выделения памяти для объектов поколения 0. Контексты выделения – небольшие области определённого сегмента кучи, каждая из которых предназначена для определённого потока исполнения.

  • Размер блока обычно составляет 8 Кб, а средний размер управляемых объектов – 35 байт. Блок выделяемой памяти – объём памяти, выделяемый распределителем каждый раз, когда ему требуется больше памяти, чтобы расположить объект внутри области. Поэтому в одном блоке можно расположить множество объектов.

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

Распределитель устроен таким образом, чтобы:

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

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

  • Он обнуляет так много памяти, чтобы подготовить кэш процессора, поскольку некоторые объекты будут размещены прямо в нём. эффективно использовать кэш: распределитель выделяет память блоками, а не для каждого объекта. Блок выделяемой памяти обычно равен 8 Кб.

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

  • обеспечивать целостность памяти: сборщик мусора всегда обнуляет память для вновь размещаемых объектов, чтобы их ссылки не указывали на произвольные участки памяти.

  • Например, если в блоке осталось 30 байт, а для размещения следующего объекта необходимо 40 байт, распределитель превратит эти 30 байт в свободный объект и запросит новый блок памяти. обеспечивать непрерывность кучи: распределитель создаёт свободный объект из оставшейся памяти в каждом выделенном блоке.

API

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 предназначен для эффективного управления памятью. Эффективное управление означает что:

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

Логическое описание управляемой кучи

После сборки объектов в поколении N, оставшиеся объекты маркируются как принадлежащие поколению N+1. Сборщик мусора CLR собирает объекты, логически разделённые по поколениям. В этом процессе существуют исключения, когда необходимо перевести объект в поколение ниже или не продвигать его вообще. Этот процесс называется продвижение объектов по поколениям.

Для больших объектов существует только одно поколение – gen3. В случае маленьких объектов куча делится на три поколения: gen0, gen1 и gen2. Gen0 и gen1 называют эфемерными поколениями (объекты живут в них короткое время).

Например, gen0 – самое молодое поколение. Для кучи небольших объектов номер поколения означает их возраст. Здесь существуют исключения, которые описаны ниже. Это не значит, что все объекты в gen0 моложе объектов в gen1 или gen2. Сборка поколения означает сборку объектов в этом поколении, а также во всех его более молодых поколениях.

Однако поскольку сжатие крупных объектов требует много ресурсов, их сборка происходит по-другому. Теоретически сборка больших и маленьких объектов может происходить одинаково. Как gen2, так и gen3 могут быть большими, а сборка объекта в эфемерных поколениях (gen0 и gen1) не должна быть слишком ресурсозатратной. Большие объекты содержатся только в gen2 и собираются только во время сборки мусора в этом поколении из соображений производительности.

Для маленьких объектов это gen0, а для больших – gen3. Объекты размещаются в самом младшем поколении.

Физическое описание управляемой кучи

Сегмент – непрерывный блок памяти, который ОС передаёт сборщику мусора. Управляемая куча состоит из набора сегментов. Сегменты каждой кучи связаны вместе. Сегменты кучи разбиваются на мелкие и большие участки для размещения маленьких и больших объектов. По крайней мере один сегмент для маленького объекта и один для большого резервируются при загрузке CLR.

Этот сегмент может содержать или не содержать объекты поколения gen2. В каждой куче маленьких объектов есть только один эфемерный сегмент, где находятся поколения gen0 и gen1. Кроме эфемерных сегментов, могут существовать один или более дополнительных сегментов, которые будут сегментами gen2, так как они содержат объекты поколения 2.

Куча больших объектов состоит из одного или более сегментов.

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

Если они не содержат используемых объектов, сегменты удаляются. Сегменты кучи выделяются по мере необходимости. За один раз для каждой кучи выделяется один сегмент. Однако начальный сегмент в куче всегда существует. Такая схема повышает производительность, поскольку большие объекты собираются только в поколении 2 (что требует много ресурсов). В случае маленьких объектов это происходит во время сборки мусора, а для больших – во время выделения памяти для них.

Последний сегмент в цепочке всегда является эфемерным. Сегменты кучи соединены вместе в порядке выделения. Повторное использование сегментов применяется только для кучи маленьких объектов. Сегменты, в которых собраны все объекты, можно использовать заново, например в качестве эфемерных. Маленькие объекты размещаются только в эфемерных сегментах. Для размещения больших объектов каждый раз рассматривается вся куча больших объектов.

Пороговое значение объёма выделенной памяти

Если он превышен, в поколении начинается сборка мусора. Это логическое понятие, связанное с размером каждого поколения.

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

Выбор поколения для сборки мусора

Кроме порогового значения, на этот выбор влияют и другие факторы: При активации сборщик должен определить, в каком поколении проводить сборку.

  • фрагментация поколения – если поколение сильно фрагментировано, сбор мусора в нём, скорее всего, будет продуктивным;
  • если память машины слишком загружена, сборщик может провести более глубокую очистку, если такая очистка с большой долей вероятности освободит пространство и позволит избежать ненужной подкачки страниц (памяти во всей машине);
  • если в эфемерном сегменте заканчивается место, сборщик может провести в этом сегменте более глубокую очистку (собрать больше объектов поколения 1), чтобы избежать выделения нового сегмента кучи.

Процесс сборки мусора

Этап маркировки

Во время этого этапа CLR должна найти все живые объекты.

Собирая мусор в эфемерных поколениях, сборщик должен получить у среды исполнения информацию о том, какие объекты в этих поколениях по-прежнему используются программой. Преимущество сборщика с поддержкой поколений заключается в его способности убирать мусор только в части кучи, вместо того, чтобы всё время наблюдать за всеми объектами. Кроме того, объекты в старших поколениях могут использовать объекты в младших поколениях, ссылаясь на них.

Биты устанавливаются механизмом JIT-компилятора во время операций присвоения. Чтобы пометить старые объекты, ссылающиеся на новые, сборщик мусора использует специальные биты. Собирая мусор в эфемерных поколениях, сборщик может использовать эти биты для всей оставшейся кучи и просмотреть только те объекты, которым эти биты соответствуют. Если объект принадлежит к эфемерному поколению, JIT-компилятор установит байт, содержащий бит, указывающий на исходное положение.

Этап планирования

Если результат оказывается продуктивным, сборщик начинает фактическое сжатие. На этом этапе моделируется сжатие, чтобы определить его эффективность. В противном случае он просто производит уборку.

Этап перемещения

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

Этап сжатия

При сжатии объекты будут скопированы по этим адресам. Этот этап достаточно прост, поскольку сборщик уже определил новые адреса для перемещения объектов во время этапа планирования.

Этап уборки

Вместо этого пространства он создаёт свободные объекты. Во время этого этапа сборщик ищет неиспользуемое пространство между живыми объектами. Все свободные объекты помещаются в список свободных объектов. Неиспользуемые объекты, находящиеся рядом, становятся одним свободным объектом.

Code flow

Термины:

  • WKS GC: Сборка мусора в режиме рабочей станции
  • SVR GC: Сборка мусора в режиме сервера

Функциональное поведение

WKS GC без параллельной сборки мусора

  1. Пользовательский поток использовал всю выделенную для него память и вызывает сборщик мусора.
  2. Сборщик вызывает SuspendEE, чтобы приостановить все управляемые потоки.
  3. Сборщик выбирает поколение для очистки.
  4. Начинается маркировка объектов.
  5. Сборщик переходит на этап планирования и определяет необходимость сжатия.
  6. При необходимости сборщик перемещает объекты и выполняет сжатие. В другом случае – просто выполняет уборку.
  7. Сборщик вызывает RestartEE, чтобы вновь запустить управляемые потоки.
  8. Пользовательские потоки продолжают работу.

WKS GC с параллельной сборкой мусора

Этот алгоритм описывает фоновую сборку мусора.

  1. Пользовательский поток использовал всю выделенную для него память и вызывает сборщик мусора.
  2. Сборщик вызывает SuspendEE, чтобы приостановить все управляемые потоки.
  3. Сборщик определяет, нужно ли запустить фоновую сборку мусора.
  4. Если да, активируется поток фоновой сборки мусора. Этот поток вызывает RestartEE, чтобы возобновить управляемые потоки.
  5. Выделение памяти для управляемых процессов продолжается одновременно с фоновой сборкой мусора.
  6. Пользовательский поток может использовать всю выделенную для него память и запустит эфемерную сборку мусора (также известную как высокоприоритетная сборка мусора). Она выполняется так же, как в режиме рабочей станции без параллельной сборки мусора..
  7. Поток фоновой сборки мусора снова вызывает SuspendEE, чтобы завершить маркировку, а затем вызывает RestartEE, чтобы запустить параллельную уборку с работающими пользовательскими потоками.
  8. Фоновая сборка мусора завершается.

SVR GC без параллельной сборки мусора

  1. Пользовательский поток использовал всю выделенную для него память и вызывает сборщик мусора.
  2. Потоки сборки мусора в режиме сервера активируются и вызывают SuspendEE, чтобы приостановить выполнение управляемых потоков.
  3. Потоки сборки мусора в режиме сервера выполняют те же операции, что и в режиме рабочей станции без параллельной сборки мусора.
  4. Потоки сборки мусора в режиме сервера вызывают RestartEE, чтобы запустить управляемые потоки.
  5. Пользовательские потоки продолжают работу.

SVR GC с параллельной сборкой мусора

Алгоритм такой же, как и в случае с параллельной сборкой мусора в режиме рабочей станции, только нефоновая сборка выполняется в серверных потоках.

Физическая архитектура

Эта секция поможет понять code flow.

Когда у пользовательского потока заканчивается память, он может получить свободное пространство с помощью функции try_allocate_more_space.

Функция try_allocate_more_space вызывает GarbageCollectGeneration, когда нужно запустить сборщик мусора.

Поток кода выглядит следующим образом: Если сборка мусора в режиме рабочей станции производится непараллельно, GarbageCollectGeneration выполняется в пользовательском потоке, который вызвал сборщик мусора.

GarbageCollectGeneration() 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(); }

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

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

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

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

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