Работа со сборщиком мусора
176C# и .NET Framework --- Оптимизация приложений .NET Framework --- Работа со сборщиком мусора
До сих пор мы рассматривали приложение, как пассивный элемент, в отношении сборщика мусора. Мы изучали реализацию сборщика мусора и знакомились с важнейшими оптимизациями, выполняемыми автоматически и не требующими практически никаких действий с нашей стороны. В этом разделе мы исследуем доступные инструменты активного воздействия на сборщика мусора, с целью повысить производительность наших приложений и получить диагностическую информацию, недоступную иными способами.
Класс System.GC
Класс System.GC - это основная точка взаимодействий со сборщиком мусора .NET из управляемого кода. Он содержит множество методов, управляющих поведением сборщика мусора и возвращающих диагностическую информацию о нем.
Диагностические методы
Диагностические методы класса System.GC возвращают информацию о состоянии сборщика мусора. Предполагается, что эти методы будут использоваться для нужд диагностики проблем и отладки; не используйте их для принятия решений при работе приложения в обычном режиме. Информация, возвращаемая этими методами, доступна также в виде счетчиков производительности из категории .NET CLR Memory.
- Метод GC.CollectionCount()
Возвращает количество циклов сборки мусора, выполненных для указанного поколения с момента запуска приложения. Его можно использовать для определения факта сборки мусора в указанном поколении в некотором интервале времени.
- Метод GC.GetTotalMemory()
Возвращает объем занятой динамической памяти в байтах. Если передать методу аргумент со значением true, предварительно будет выполнена полная сборка мусора, чтобы возвращаемое значение точно соответствовало объему памяти, которая не может быть освобождена.
- Метод GC.GetGeneration()
Возвращает номер поколения, которому принадлежит указанный объект. Обратите внимание, что в текущей реализации CLR объекты не всегда перемещаются между поколениями при сборке мусора.
Уведомления
Появившийся в версии .NET 3.5 SP1 прикладной интерфейс уведомлений сборщика мусора дает приложениям возможность заранее узнать о надвигающейся полной сборке мусора. Этот прикладной интерфейс доступен только при использовании непараллельного сборщика мусора и предназначен для использования в приложениях, в которых наблюдаются длительные паузы, вызванные сборкой мусора, и где требуется реализовать распределение работы или рассылку уведомлений, когда ожидается наступление очередной паузы.
Сначала приложение, заинтересованное в получении уведомлений от сборщика мусора, вызывает метод GC.RegisterForFullGCNotification() и передает ему два пороговых значения (числа от 1 до 99). Эти значения указывают, как рано приложение должно извещаться о наступлении сборки мусора, и основаны на пороговых значениях поколения 2 и кучи больших объектов. Проще говоря, чем больше значение, тем больше временной разрыв между уведомлением и фактической сборкой мусора. При маленьких значениях появляется риск не получить уведомление из-за того, что разрыв времени окажется слишком маленьким и механизм уведомлений просто не успеет сработать.
Затем приложение вызывает метод GC.WaitForFullGCApproach(), который блокируется до появления уведомления, выполняет подготовительные операции перед сборкой мусора и вызывает метод GC.WaitForFullGCComplete(), который блокируется до окончания сборки мусора. Поскольку все методы выполняются синхронно, есть смысл вызывать их в фоновом потоке и возбуждать событие в основном потоке выполнения программы, как показано ниже:
public class GCWatcher
{
private Thread watcherThread;
public event EventHandler GCApproaches;
public event EventHandler GCComplete;
public void Watch()
{
GC.RegisterForFullGCNotification(50, 50);
watcherThread = new Thread(() =>
{
while (true)
{
GCNotificationStatus status = GC.WaitForFullGCApproach();
// Код обработки ошибок опущен
if (GCApproaches != null)
{
GCApproaches(this, EventArgs.Empty);
}
status = GC.WaitForFullGCComplete();
// Код обработки ошибок опущен
if (GCComplete != null)
{
GCComplete(this, EventArgs.Empty);
}
}
});
watcherThread.IsBackground = true;
watcherThread.Start();
}
public void Cancel()
{
GC.CancelFullGCNotification();
watcherThread.Join();
}
}
3a дополнительной информацией и подробными примерами использования прикладного интерфейса уведомлений для распределения нагрузки на сервер обращайтесь к документации на сайте MSDN: "Уведомления о сборке мусора".
Управляющие методы
Метод GC.Collect() предписывает сборщику мусора выполнить сборку в указанном поколении (включая все более молодые поколения). Начиная с версии .NET 3.5 (а также в версиях .NET 2.0 SP1 и .NET 3.0 SP1), метод GC.Collect() был перегружен параметром типа перечисления GCCollectionMode. Это перечисление определяет следующие значения:
- GCCollectionMode.Forced
Вынуждает сборщика мусора немедленно выполнить сборку, синхронно с текущим потоком выполнения. Метод возвращает управление только по завершении сборки мусора.
- GCCollectionMode.Optimized
Позволяет сборщику мусора самому решить, насколько оправданно будет выполнить сборку в данный момент. Сборщик мусора может решить отложить сборку. Это рекомендуемый режим, если вашей целью является помочь сборщику мусора, подсказывая, когда сборка мусора является желательной. Для диагностики или когда требуется принудительно выполнить полную сборку мусора, чтобы удалить определенный объект, используйте значение GCCollectionMode.Forced.
- GCCollectionMode.Default
Начиная с версии CLR 4.5 значение GCCollectionMode.Default действует эквивалентно значению GCCollectionMode.Forced.
Принудительная сборка мусора редко применяется на практике. Оптимизации, описанные в предыдущих статьях, в значительной степени опираются на динамические настройки и эвристики, хорошо протестированные в приложениях самых разных типов. Мы не рекомендуем вызывать принудительную сборку мусора и даже советовать сборщику мусора начать ее (передачей значения GCCollectionMode.Optimized) без явной необходимости. А теперь, после всего сказанного выше, можно обозначить ситуации, когда можно подумать об использовании возможности выполнять принудительную сборку мусора:
По завершении нечастых операций, требующих больших объемов памяти, эта память может быть освобождена. Если приложение не вызывает полную сборку мусора достаточно часто, занятая память может довольно долго оставаться в поколении 2 (или в куче больших объектов). В этой ситуации, когда достоверно известно, что большой объем памяти больше не будет использоваться, имеет смысл принудительно запустить сборку мусора, чтобы избежать вытеснения страниц оперативной памяти в файл подкачки.
При использовании сборщика мусора в режиме работы с низкими задержками, желательно принудительно запустить сборку мусора в безопасной точке программы, когда известно, что все критичные ко времени операции уже выполнены и приложение может позволить себе небольшую паузу для сборки мусора. Длительная работа в режиме с низкими задержками без выполнения сборки мусора может привести к исчерпанию памяти. Вообще говоря, если приложение слишком чувствительно к выбору моментов времени на сборку мусора, разумно принудительно запускать сборку мусора в периоды простоя, чтобы избежать лишних накладных расходов в периоды активных действий.
Когда для освобождения неуправляемых ресурсов используется недетерминированная финализация, часто бывает желательно приостановить работу, пока все такие ресурсы не будут освобождены. Этого можно добиться последовательным вызовом методов GC.Collect() и GC.WaitForPendingFinalizers(). В таких ситуациях всегда предпочтительнее использовать детерминированную финализацию, но зачастую у нас отсутствует возможность управлять внутренними классами, выполняющими фактическую финализацию.
В версии .NET 4.5 появилась еще одна перегруженная версия метода GC.Collect() с дополнительным логическим параметром: GC.Collect(int generation, GCCollectionMode mode, bool blocking). Этот параметр управляет необходимостью блокировки на время выполнения сборки мусора (по умолчанию включена, в противном случае сборка выполняется асинхронно, в фоновом потоке).
Ниже перечислены другие методы управления сборщиком мусора:
- Методы GC.AddMemoryPressure() и GC.RemoveMemoryPressure()
Их можно использовать для уведомления сборщика мусора о выделении или освобождении неуправляемой памяти в текущем процессе. Уведомление о выделении памяти сообщает сборщику мусора, какой объем неуправляемой памяти был выделен процессу. Сборщик мусора может использовать эту информацию для настройки агрессивности и частоты сборки мусора или игнорировать ее. При освобождении неуправляемой памяти можно уведомить сборщик мусора о снятии лишнего давления на память.
- GC.WaitForPendingFinalizers()
Этот метод блокирует выполнение текущего потока, пока не завершатся все методы-финализаторы. Этот метод следует использовать с большой осторожностью, потому что он может приводить к взаимоблокировкам. Например, если главный поток выполнения окажется заблокирован в вызове GC.WaitForPendingFinalizers() в тот момент, когда он удерживает некоторую блокировку, и один из активных финализаторов попытается приобрести ту же самую блокировку, возникнет состояние взаимоблокировки. Так как метод GC.WaitForPendingFinalizers() не имеет параметра, ограничивающего предельное время ожидания, финализаторы, пытающиеся приобрести блокировку, должны использовать таймаут для обработки ошибочных ситуаций.
- Методы GC.SuppressFinalize() и GC.ReRegisterForFinalize()
Используются совместно с механизмами финализации и воскрешения объектов.
Начиная с версии .NET 3.5 (доступен также в версиях .NET 2.0 SP1 и .NET 3.0 SP1) появился еще один интерфейс к сборщику мусора в виде класса GCSettings, который позволяет управлять поведением сборщика мусора и переключаться в режим с низкой задержкой. Описание других методов и свойств класса System.GC, не упомянутых в этой статье, ищите в документации на сайте MSDN.
Взаимодействие с применением интерфейсов размещения CLR
В предыдущем разделе мы исследовали методы диагностики и управления сборщиком мусора, доступные из управляемого кода. Однако степень контроля, предлагаемая этими методами, оставляет желать лучшего. Кроме того, нет никакого механизма уведомлений, с помощью которого можно было бы узнать, когда запускается сборка мусора.
Эти проблемы невозможно решить с применением только управляемого кода. Поэтому, для реализации более полного управления сборщиком мусора необходимо выполнить размещение CLR. Этот прием обеспечивает множество механизмов управления памятью в .NET.
Интерфейс IHostMemoryManager и связанный с ним интерфейс IHostMalloc объявляют методы обратного вызова, которые среда выполнения CLR использует для выделения сегментов сборщику мусора, для приема уведомлений о нехватке памяти, для выделения памяти, неподконтрольной сборщику мусора (например, для кода, сгенерированного JIT-компилятором) и для оценки доступного объема свободной памяти. Например, этот интерфейс можно использовать, чтобы обеспечить удовлетворение всех запросов на выделение памяти из областей ОЗУ, которые не могут вытесняться в файл подкачки. В этом заключается суть открытого проекта Non-Paged CLR Host.
Интерфейс ICLRGCManager объявляет методы управления сборщиком мусора и получения статистических данных о его работе. Его можно использовать для запуска сборки мусора из размещающего кода, для получения статистики (доступной также посредством счетчиков производительности в категории .NET CLR Memory) и для инициализации начальных пороговых значений сборщика мусора, включая размер сегмента сборщика мусора и максимальный размер поколения 0.
Интерфейс IHostGCManager объявляет методы для приема уведомлений о запуске и окончании сборки мусора, а также о приостановке потока выполнения, чтобы сборщик мусора мог продолжить работу.
Ниже приводится небольшой фрагмент кода из открытого проекта Non-Paged CLR Host, демонстрирующий, как можно выполнить настройку выделения сегментов по запросам размещаемой среды выполнения CLR и как обеспечить выделение страниц памяти, не вытесняемых в файл подкачки:
Триггеры сборщика мусора
Мы познакомились с рядом причин, вызывающих сборку мусора, но мы нигде не перечисляли их вместе. Ниже приводится список триггеров, используемых средой CLR для определения необходимости запуска сборки мусора, перечисленных в порядке убывания их значимости:
Заполнение области памяти для поколения 0. Это происходит всякий раз, когда приложение создает новый объект в небольшой области памяти.
Заполнение кучи больших объектов достигло порогового значения. Это происходит при создании больших объектов.
Явный вызов метода GC.Collect().
Операционная система сообщила о нехватке памяти. Для слежения за утилизацией памяти и соблюдения типичных требований, предъявляемых операционной системой, среда CLR использует Win32 API-уведомления.
Выгрузка экземпляра AppDomain.
Завершение процесса (или CLR). В этом случае производится вырожденная сборка мусора - ничто не интерпретируется как корень, объекты не переносятся в старшие поколения и сжатие динамической памяти не производится. Основная цель этого цикла сборки мусора - выполнить методы-финализаторы.