Содержание
Память, занятая неиспользуемыми объектами, должна освобождаться. Этот процесс выполняется средой CLR автоматически и называется сборкой мусора. Однако помимо управляемой памяти существуют и другие машинные ресурсы, которые может использовать программа: файлы, дескрипторы ОС, сетевые подключения и т.д. Эти ресурсы сборщиком мусора не освобождаются. Типы, инкапсулирующие работу с машинными ресурсами, должны освобождать эти ресурсы после завершения работы с ними самостоятельно. Освобождение реализуется с помощью интерфейса IDisposable
. Освобождение вызывается явно, в то время как сборка мусора полностью автоматизирована и не требует никаких действий со стороны программиста.
IDisposable, Dispose и Close
В .NET определен специальный интерфейс для типов, требующих освобождения ресурсов:
1 2 3 4 | public interface IDisposable { void Dispose(); } |
Оператор using
предлагает синтаксическое сокращение для вызова метода Dispose
на объектах, реализующих интерфейс IDisposable
, используя блок try
/finally
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open)) { ... } // Преобразуется компилятором в: FileStream fs = new FileStream ("myFile.txt", FileMode.Open); try { .... } finally { if (fs != null) ((IDisposable)fs).Dispose(); } |
Блок finally
гарантирует, что метод Dispose
будет вызван даже в случае генерации исключения при работе с машинным ресурсом.
При реализации интерфейса IDisposable
рекомендуется придерживать следующих правил (хотя они и не являются обязательными):
- после освобождения (вызова метода
Dispose
) объект становится недоступным: его нельзя активировать вновь, обращение к его методам и свойствам генерирует исключениеObjectDisposedException
- многократный вызов метода
Dispose
не должен приводить к ошибкам - если освобождаемый объект, содержит другие объекты, реализующие интерфейс
IDisposable
, эти объекты также должны освобождаться, т.е. методDispose
родительского объекта должен вызывать методыDispose
дочерних объектов
Некоторые типы помимо метода Dispose
определяют методы Close
или Stop
. Эти методы как правило идентичны методу Dispose
и тоже выполняют освобождение, но как правило объекты, освобожденные с помощью этих методов, могут быть реанимированы методами Open
или Start
.
Метод Dispose
помимо собственно освобождения машинных ресурсов может выполнять и другие действия, необходимые для уничтожения объекта. Например методDispose
может очищать поля объекта, отменять подписку на события, очищать собственные обработчики событий (устанавливая их в null
), уничтожать ценную секретную информацию, хранящуюся в объекте (например, ключи шифрования). Хорошей практикой также является установка после освобождения специального поля, указывающего на то, что объект освобожден:
1 | public bool IsDisposed { get; private set; } |
Методы объекта при этом проверяют данное поле и если оно возвращает true
, методы генерируют исключение ObjectDisposedException
, указывающее, что объект недоступен вследствие освобождения.
Действия, выполняемые во время освобождения, условно подразделяются на существенные и несущественные. В соответствии с шаблоном подключаемого освобождения, пользователь должен иметь возможность выполнить при освобождении только существенные действия и отказаться от выполнения несущественных. С этой целью обычно создается поле, проверяемое методом Dispose
, и если поле возвращаетtrue
, метод выполняет полную очистку, а если false
— только существенную. Пользователь типа соответственно может переключать это поля, указывая нужно ли выполнять несущественную очистку или нет:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public sealed class HouseManager : IDisposable { public readonly bool CheckMailOnDispose; public Demo (bool checkMailOnDispose) { CheckMailOnDispose = checkMailOnDispose; } public void Dispose() { if (CheckMailOnDispose) CheckTheMail(); LockTheHouse(); } } |
Автоматическая сборка мусора
Независимо от того, требует ли объект освобождения используемых машинных ресурсов, память, занимаемая им в хипе, должна быть освобождена. Среда CLR делает это автоматически с помощью автоматического сборщика мусора. Самостоятельно освобождать управляемую память не требуется.
Например, применительно к следующему методу:
1 2 3 4 5 | public void Test() { byte[] myArray = new byte[1000]; ... } |
Когда метод Test
выполняется массив распределяется в хипе. Ссылка на массив, содержащаяся в локальной переменной myArray
, храниться в стеке. Когда метод завершается, локальная переменная myArray
покидает область видимости, и на массив в хипе, на который она ссылалась, больше не остается ссылок. Такой недоступный (осиротевший) массив может быть утилизирован при сборке мусора.
Сборка мусора не происходит немедленно после того, как объект становится недоступным. Она выполняется периодически: среда CLR основывает свое решение о том, когда нужно провести сборку мусора, на ряде факторов: объем доступной памяти и время прошедшее с последней сборки мусора. Поэтому между моментом, когда объект становится недоступным, и моментом, когда занимаемая им память будет освобождена, имеется неопределенная задержка: от наносекунд до нескольких дней.
Сборщик мусора не собирает весь мусор при каждой сборке. Вместо этого диспетчер памяти разделяет объекты на поколения, и сборщик мусора выполняет сборку новых поколений (недавно распределенных объектов) чаще, чем старых поколений (объектов, существующих на протяжении длительного времени).
Корни (Roots)
Корень (корневой объект) — это то, что сохраняет объект в активном состоянии, если на объект нет прямой или косвенной ссылки со стороны корня, он будет доступен для сборки мусора. Корнем может выступать:
- локальная переменная или параметр в выполняющемся методе
- статическая переменная
- объект в очереди на финализацию
Объекты, циклически ссылающиеся друг на друга, считаются недостижимыми если на них отсутствует ссылка из корня.
Финализаторы
Перед тем, как объект будет удален из памяти запускается его финализатор, если он определен. Финализаторы объявляются как конструкторы, но с префиксом ~
. Финализатор не может быть public
или static
, не может принимать параметры и обращаться к базовому классу.
Сборка мусора проходит несколько фаз. Сначала сборщик мусора идентифицирует неиспользуемые объекты готовые к удалению. Объекты без финализаторов удаляются сразу. Объекты с финализаторами сохраняются в активном состоянии и помещаются в специальную очередь. На этом сборка мусора завершается и программа продолжает выполнение. При этом параллельно начинает выполняться поток финализации, выбирая объекты из указанной очереди и запуская их методы финализации. Перед запуском финализатора объект по прежнему активен — очередь действует в качестве корня. После того как объект извлечен из очереди, а его финализатор выполнен, объект становится недоступным и будет удален при следующей сборке мусора.
Финализаторы могут быть полезны, но есть ряд отрицательных моментов в их использовании:
- финализаторы замедляют выделение и утилизацию памяти
- финализаторы продлевают время жизни объектов (а также ссылающихся на них объектов)
- порядок вызова финализаторов предсказать невозможно
- момент вызова финализатора контролировать невозможно
- если финализатор приводит к блокировке, другие объекты не смогут выполнить финализацию
- финализатор может вообще не запуститься если приложение завершиться аварийно
Чтобы минимизировать отрицательные моменты, финализаторы должны работать быстро, не блокироваться, не ссылаться на другие финализируемые объекты, не генерировать исключений.
Вызов Dispose из финализатора
Существует популярный шаблон программирования, заключающийся в том, что финализатор вызывает Dispose
. Шаблон используется в основном как страховка, если потребитель забудет вызвать Dispose
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Test : IDisposable { public void Dispose() // НЕ виртуальный { Dispose (true); GC.SuppressFinalize (this); // Отменяет вызов финализатора } protected virtual void Dispose (bool disposing) { if (disposing) { // Здесь можно ссылаться на другие финализируемые объекты // ... } // Освобождение ресурсов, которыми владеет только этот объект // ... } ˜Test() { Dispose (false); } } |
Метод Dispose
перегружен для приемы флага disposing
типа bool
. Версия без параметров не объявлена как virtual
и просто вызывает расширенную версию с передачей ей true
.
Расширенная версия содержит действительную логику освобождения. Она помечена как protected
и virtual
, давая производным классам реализовывать свою логику освобождения. Флаг disposing
означает, что расширенная версия вызвана правильно из метода Dispose
, а не в режиме крайнего случая из финализатора.
Вызов GC.SuppressFinalize
в методе Dispose
без параметров удаляет финализатор, и когда сборщик мусора доберется до объекта он не будет запускать финализатор и удалит объект сразу. Это позволяет улучшить производительность: еслиDispose
был вызван не аварийно, нет смысла вызывать его повторно в финализаторе.
Финализатор может модифицировать активный объект так, что он начнет ссылаться на неактивный объект. Во время очередной сборки мусора среда CLR выяснит, что ранее неактивный объект больше не является недоступным, поэтому он должен избежать сборки мусора. Этот прием называется восстановлением (воскрешением). Финализатор восстановленного объекта не запуститься повторно, если только не вызвать GC.ReRegisterForFinalize
.
Процесс сборки мусора
Процесс сборки мусора может быть запущен при распределении памяти (когда создается экземпляр с помощью ключевого слова new
), либо после выделения определенного порога памяти, либо в другие моменты. Цель сборки мусора — уменьшить объем памяти, занимаемой приложением. Сборку мусора можно активировать вручную, вызвав System.GC.Collect
.
Сборка мусора начинается с того, что сборщик мусора перебирает корневые ссылки и помечает объекты, не которые они ссылаются, как достижимые. После завершения этого процесса все непомеченные объекты считаются недостижимыми и подлежат сборке мусора.
Недостижимые объекты без финализатора уничтожаются немедленно, а объекты с финализатором помещаются в очередь финализации, для выполнения их финализатров после завершения сборки мусора параллельно с выполнением программы. После завершения потока финализации эти объекты считаются доступными для сборки мусора при следующем запуске сборщика.
Оставшиеся активные объекты затем сдвигаются в начало хипа (сжатие), освобождая пространство под новые объекты. Сжатие устраняет фрагментацию памяти и упрощает процесс выделения памяти под новые объекты.
Сборщик мусора поддерживает различные технологии оптимизации, для сокращения времени сборки мусора.
Поколения при сборке мусора
Идея поколений основывается на том, что некоторые объекты имеют краткий жизненный цикл, другие же существуют длительное время и отслеживать их при каждой сборке мусора необязательно. Сборщик мусора делит хип на три поколения. Недавно созданные объекты относятся к поколению Gen0, объекты выдержавшие один цикл сборки мусора — к поколению Gen1, а все остальные объекты принадлежат к поколению Gen2.
Для поколения Gen0 среда CLR выделяет относительно небольшой участок памяти (16Мб). Когда эта память заканчивается, инициируется сборка мусора для поколения Gen0, что происходит довольно часто. Для поколения Gen1 используется похожий лимит памяти, поэтому сборка мусора для Gen1 тоже происходит довольно часто. Полная же сборка мусора, включающая поколение Gen2, является довольно медленной и происходит редко.
LOH
Для объектов, размер которых превышает определенный порог (85 000 байт), сборщик мусора использует отдельную область, называемую хипом для больших объектов — Large Object Heap, LOH. Большие объекты способны быстро заполнить поколение Gen0, и при отсутствии LOH сборка мусора для этого поколения запускалась бы чаще, снижая производительность.
Область LOH не подвержена сжатию, т.к. перемещение больших объектов является ресурсоемким. По этой причине выделение памяти под новые объекты является более медленным, т.к. сборщик мусора должен просматривать свободные участки памяти. Кроме того LOH подвержена фрагментации.
LOH не поддерживает концепцию поколений: все объекты рассматриваются как относящиеся к поколению Gen2.
Параллельная и фоновая сборка мусора
Сборщик мусора блокирует потоки выполнения на период сборки мусора для поколений Gen0 и Gen1. Однако сборка мусора для поколения Gen2 протекает параллельно или в фоновом режиме. Эта оптимизация актуальна только для рабочих станций, для серверов сборка поколения Gen2 также блокирует выполнение потоков.
Уведомления о сборке мусора
Серверная версия CLR может отправлять уведомления непосредственно перед началом полной сборки мусора. Это позволяет переадресовывать запросы на другой сервер на время проведения сборки мусора.
Принудительный запуск сборки мусора
Принудительно запустить сборку мусора можно в любой момент вызвав GC.Collect
. Вызов без аргументов инициирует полную сборку мусора. Если передать целочисленное значение, сборка мусора будет выполнена для поколений с Gen0 до поколения, номер которого соответствует переданному значению.
Вызов метода WaitForPendingFinalizers
позволяет отложить дальнейшее выполнение до завершения потока финализации. Таким образом вызов:
1 2 3 | GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); |
Позволяет выполнить сборку мусора для объектов с финализаторами.
Слабые ссылки
Иногда полезно иметь ссылку на объект, которая является невидимой для сборщика мусора и соответственно не может сохранить объект в активном состоянии и уберечь его от сборки мусора. Такие ссылки называются слабыми и реализуются с помощью класса System.WeakReference
:
1 2 3 | var sb = new StringBuilder ("this is a test"); var weak = new WeakReference (sb); Console.WriteLine (weak.Target); // This is a test |
Target
экземпляра WeakReference
станет равным null
:1 2 3 4 | var weak = new WeakReference (new StringBuilder ("weak")); Console.WriteLine (weak.Target); // weak GC.Collect(); Console.WriteLine (weak.Target); // null |
1 2 | var weak = new WeakReference (new StringBuilder ("weak")); var sb = (StringBuilder) weak.Target; |