Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Базовые технологии платформы .NET.docx
Скачиваний:
13
Добавлен:
03.11.2018
Размер:
614.46 Кб
Скачать

6. Жизненный цикл объектов

Все типы платформы .NET делятся на ссылочные типы и типы значений. Переменные типов значений создаются в стеке, и их время жизни ограничено тем блоком кода, в котором они объявляются. Например, если переменная типа значения объявлена в некотором методе, то после выхода из метода память в стеке, занимаемая переменной, автоматически освободится.

Переменные ссылочного типа (объекты) располагаются в динамической памяти – «управляемой куче» (managed heap). Размещение объектов в «куче» происходит последовательно. Для этого CLR поддерживает указатель на свободное место в куче, перемещая его на соответствующее количество байт после выделения памяти очередному объекту.

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

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

  • локальная переменная или параметр выполняемого метода (а также всех методов в стеке вызова);

  • статическое поле;

  • объект в очереди завершения (этот термин будет разъяснён позже).

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

При размещении и удалении объектов CLR использует ряд оптимизаций. Во-первых, объекты размером более 85 Кб размещаются в отдельной управляемой куче. При сборке мусора данная куча не дефрагментируется, так как копирование больших блоков памяти снижает производительность. Во-вторых, управляемая куча для малых объектов выделяет три поколения объектов – Gen0, Gen1 и Gen2. Вначале все объекты в куче относятся к Gen0. После первой сборки мусора те объекты, которые не были удалены, переходят в поколение Gen1, а новые объекты будут принадлежать Gen0. Вторая сборка мусора порождает поколение Gen2. Процесс сборки мусора работает с объектами старших поколений, только если освобождение памяти в младших поколениях дало неудовлетворительный результат.

Финализаторы и интерфейс iDisposable

Обсудим автоматическую сборку мусора с точки зрения программиста, разрабатывающего некий класс. С одной стороны, такой подход имеет свои преимущества. В частности, практически исключаются случайные утечки памяти, которые могут вызвать «забытые» объекты. С другой стороны, объект может захватить некоторые особо ценные ресурсы (например, подключения к базе данных), которые требуется освободить сразу после того, как объект перестаёт использоваться. В такой ситуации выходом является написание особого метода, который содержит код освобождения ресурсов.

Но как гарантировать освобождение ресурсов, даже если ссылка на объект была случайно утеряна? Класс System.Object содержит виртуальный метод Finalize(). Если объект пользовательского класса при работе захватывает ресурсы, класс может переопределить метод Finalize() для их освобождения. Объекты таких классов при сборке мусора обрабатываются особо. Когда сборщик мусора распознаёт, что уничтожаемый объект имеет собственную реализацию метода Finalize(), он помещает такой объект в специальную очередь завершения. Затем в отдельном программном потоке у объектов из очереди завершения происходит вызов метода Finalize() и фактическое уничтожение.

Язык C# не позволяет явно переопределить в пользовательском классе метод Finalize(). Вместо переопределения Finalize() в классе описывается специальный финализатор. Имя финализатора имеет вид ~<имя класса>, он не имеет параметров и модификаторов доступа (считается, что модификатор доступа финализатора ‑ protected, а значит, его нельзя явно вызвать у объекта). При наследовании в финализатор класса-наследника автоматически подставляется вызов финализатора класса-предка.

Рассмотрим пример класса с финализатором:

public class ClassWithFinalizer

{

public void DoSomething()

{

Console.WriteLine("I'm working...");

}

~ClassWithFinalizer()

{

Console.WriteLine("Bye!");

}

}

Приведём пример программы, использующей этот класс:

public class MainClass

{

private static void Main()

{

var A = new ClassWithFinalizer();

A.DoSomething();

// cборка мусора запуститься перед завершением приложения

}

}

Данная программа выводит следующие строки:

I'm working...

Bye!

Проблема с использованием финализатора заключается в недетерминированности его вызова. Программист может описать в классе некий метод, который следует вызывать «вручную», когда объект больше не нужен. Для унификации данного решения платформа .NET предлагает интерфейс IDisposable, содержащий единственный метод Dispose(), куда помещается завершающий код работы с объектом.

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

public class DisposePattern : IDisposable

{

public void Dispose() // реализация IDisposable

{

Dispose(true);

// этот вызов подавляет запуск финализатора у объекта

GC.SuppressFinalize(this);

}

~DisposePattern() // реализация финализатора

{

Dispose(false);

}

protected virtual void Dispose(bool disposing)

{

if (disposing)

{

// вызов из Dispose() – полное контролируемое удаление

}

// этот блок выполняется только для вызова из финализатора

}

}

Язык C# имеет специальный оператор using, который гарантирует вызов метода Dispose() для объектов, используемых в своём блоке. Синтаксис оператора using следующий:

using (<имя объекта или объявление и создание объектов>) <блок кода>

Продемонстрируем использование оператора using:

using (var A = new DisposePattern())

{

A.DoSomething();

// компилятор поместит сюда вызов A.Dispose()

}

Сборщик мусора представлен статическим классом System.GC, который обладает несколькими полезными методами (это неполный список):

  • Collect() ‑ вызывает принудительную сборку мусора в программе.

  • GetGeneration() – возвращает поколение для указанного объекта;

  • SuppressFinalize() ‑ подавляет вызов финализатора для указанного объекта;

  • WaitForPendingFinalizers() ‑ приостанавливает текущий поток выполнения до тех пор, пока не будут выполнены все финализаторы освобождаемых объектов.