Московский государственный технический университет им. Н.Э. Баумана
Опубликован: 28.06.2006 | Доступ: свободный | Студентов: 12463 / 341 | Оценка: 4.54 / 3.83 | Длительность: 22:03:00
ISBN: 978-5-9556-0055-0
Лекция 2:

Виртуальная система выполнения. Автоматическое управление памятью

< Лекция 1 || Лекция 2: 123 || Лекция 3 >

Автоматическое управление памятью

Одной из основных особенностей платформы .NET, делающих ее привлекательной для разработки приложений, является механизм автоматического управления памятью, известный как сборка мусора (garbage collection).

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

В .NET реализован так называемый сборщик мусора с поколениями (generational garbage collector), работающий на основе построения графа достижимости объектов.

Выделение памяти в управляемой куче

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

Для некоторых объектов определены методы Finalize, выполняющие некие действия при удалении объекта из кучи. Эти методы являются аналогами деструкторов языка C++ и используются главным образом для освобождения системных ресурсов, связанных с объектами. В целях повышения эффективности сборщика мусора при выделении памяти для объекта, имеющего метод Finalize, адрес этого объекта заносится в список завершения (finalization list).

Если сравнить механизм выделения памяти в управляемой куче .NET с работой функции malloc языка C, можно прийти к выводу, что функция malloc работает гораздо менее эффективно. Причина в том, что исполняющая среда языка C организует кучу в виде связного списка блоков памяти. При этом размеры блоков в общем случае различны. Функции malloc приходится выполнять поиск свободного блока нужного размера, разбивать этот блок и затем вносить необходимые изменения в список блоков. Ясно, что выполнение этих действий требует значительно больше времени, чем простое увеличение указателя HeapPtr.

Алгоритм сборки мусора

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

  • она исполняет не CIL-код, а порожденный JIT-компилятором код процессора семейства Intel x86;
  • для каждого потока выполнения существует стек, в котором расположены фреймы вызванных методов. Каждый фрейм содержит адрес возврата, адрес фрейма предыдущего метода в стеке, а также локальные переменные и параметры метода;
  • стеки вычислений в явном виде отсутствуют. Вместо них используются регистры процессора и стек потока;
  • объектные ссылки представляют собой обычные указатели на объекты в управляемой куче.

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

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

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

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

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

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

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

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

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

Основные приемы повышения эффективности сборки мусора

Проведение сборки мусора только для части объектов кучи позволяет существенно сократить время работы сборщика. Поэтому все объекты делятся на три категории, называемые поколениями. В поколении 0 сборка мусора проводится чаще всего. Объекты, пережившие сборку мусора в поколении 0, переводятся в поколение 1, в котором сборка мусора осуществляется реже. Объекты, не удаленные после сборки мусора в поколении 1, переводятся в поколение 2. Сборка мусора в поколении 2 выполняется совсем редко.

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

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

< Лекция 1 || Лекция 2: 123 || Лекция 3 >
Анастасия Булинкова
Анастасия Булинкова
Рабочим названием платформы .NET было