Эндрю Троелсен - ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2005 И ПЛАТФОРМА .NET 2.0. 3-е издание
ГЛАВА 5. Цикл существования объектов
В предыдущей главе мы потратили достаточно много времени на то, чтобы научиться строить пользовательские типы класса в C#. В этой главе мы выясним, как среда CLR управляет уже размешенными объектами с помощью процесса, который называется сборкой мусора. Программистам использующим C# не приходится удалять объекты из памяти "вручную" (напомним, что в C# вообще нет ключевого слова delete). Объекты .NET размещаются в области памяти, которая называется управляемой динамической памятью, где эти объекты "в некоторый подходящий момент" будут автоматически уничтожены сборщиком мусора.
Выяснив основные детали процесса сборки мусора, вы узнаете, как взаимодействовать со сборщиком мусора, используя для этого тип класса System.GC, Наконец, мы рассмотрим виртуальный метод System.Object.Finalize() и интерфейс IDisposable, которые можно использовать для того, чтобы создавать типы самостоятельно освобождающие в нужный момент свои внутренние неуправляемые ресурсы. Изучив материал этой главы, вы сможете понять, как среда CLR управляет объектами .NET.
Классы, объекты и ссылки
Чтобы очертить контуры темы, рассматриваемой в данной главе, необходимо уточнить различия между класcами, объектами и ссылками. В предыдущей главе уже говорилось о том, что класс – это своеобразный "шаблон" с описанием того, как экземпляр данного типа должен выглядеть и вести себя в памяти. Классы определяются в файлах, которые по соглашению в C# имеют расширение *.cs. Рассмотрим простой класс Car (автомобиль), определённый в файле Car.cs.
public class Car {
private int currSp;
private string petName;
public Car(){}
public Car(string name, int speed) {
petName = name;
currSp = speed;
}
public override string ToString() {
return String.Format("{0} имеет скорость {1} км/ч", petName, currSp);
}
}
Определив класс, вы можете разместить в памяти любое число соответствующих объектов, используя ключевое слово C# new. При этом, однако, следует понимать, что ключевое слово new возвращает ссылку на объект в динамической памяти, а не сам реальный объект. Эта переменная со ссылкой запоминается в стеке для использования в приложении в дальнейшем. Для вызова членов объекта следует применить к сохраненной ссылке операцию C#, обозначаемую точкой.
class Program {
static void Main(string[] args) {
// Создается новый объект Car в динамической памяти.
// Возвращается ссылка на этот объект ('refТоМуСаr').
Car refToMyCar = new Car("Zippy", 50);
// Операция C#, обозначаемая точкой (.), используется
// со ссылочной переменной для вызова членов этого объекта.
Console.WriteLine(refToMyCar.ToString());
Console.ReadLine();
}
}
На рис. 5.1 изображена схема, иллюстрирующая взаимосвязь между классами, объектами и их ссылками.
Рис. 5.1. Ссылки на объекты в управляемой динамической памяти
Основные сведения о существовании объектов
При построении C#-приложений вы вправе предполагать, что управляемая динамическая память будет обрабатываться без вашего прямого вмешательства. "Золотое правило" управления памятью .NET является очень простым.
• Правило. Следует поместить объект в управляемую динамическую память с помощью ключевого слова new и забыть об этом.
Сборщик мусора уничтожит созданный объект, когда этот объект будет больше не нужен. Cледующий очевидный вопрос: "Как сборщик мусора определяет, что объект больше не нужен?" Краткий (т.е. упрощенный) ответ заключается в том, что сборщик мусора удаляет объект из динамической памяти тогда, когда объект становится недоступным для всех частей программного кода. Предположим, что вы имеете метод, размещающий локальный объект Car.
public static void MakeACar() {
// Если myCar является единственной ссылкой на объект Car,
// то объект может быть уничтожен после возвращения из метода.
Car myCar = new Car();
…
}
Обратите внимание на то, что ссылка на объект (myCar) была создана непосредственно в методе MakeACar() и не передавалась за пределы области видимости определяющего эту ссылку объекта (ни в виде возвращаемого значения, ни в виде параметров ref/out). Поэтому после завершения работы вызванного метода ссылка myCar становится недоступной, и соответствующий объект Car оказывается кандидатом для удаления в "мусор". Однако следует понимать, что вы не можете гарантировать немедленное удаление этого объекта из памяти сразу же по завершении работы MakeACar(). В этот момент можно гарантировать только то, что при следующей сборке мусора в общеязыковой среде выполнения (CLR) объект myCar может быть без опасений уничтожен.
Вы, несомненно, обнаружите, что программирование в окружении, обеспечивающем автоматическую сборку мусора, значительно упрощает задачу разработки приложений. Программисты, использующие C++, знают о том, что если в C++ забыть вручную удалить размещенные в динамической памяти объекты, может произойти "утечка памяти". На самом деле ликвидация утечек памяти является одним из самых трудоемких (и неинтересных) аспектов программирования на языках, которые не являются управляемыми. Поручив сборщику мусора уничтожение объектов, вы снимаете с себя груз ответственности за управление памятью и перекладываете его на CLR.
Замечание. Если вы имеете опыт разработки программ в использованием COM, то знайте, что объекты .NET не поддерживают счетчик внутренних ссылок, поэтому управляемые объекты не предлагают такие методы, как AddRef() и Release().
CIL-код для new
Когда компилятор C# обнаруживает ключевое слово new, он генерирует CIL-инструкцию newobj в рамках реализации соответствующего метода. Если выполнить компиляцию программного кода текущего примера и с помощью ildasm.exe рассмотреть полученный компоновочный блок, то в рамках метода MakeACar() вы увидите следующие CIL-операторы.
.method public hidebysig static void MakeACar() cil managed
{
// Code size 7 (0x7)
.maxstack 1
.locals init ([0] class SimpleFinalize.Car c)
IL_0000: newobj instance void SimpleFinalize.Car::.ctor()
IL_0005: stloc.0
IL_0006: ret}
} // end of method Program::MakeACar
Перед тем как обсудить точные правила, определяющие момент удаления объекта из управляемой динамической памяти, давайте выясним роль CIL-инструкции newobj. Сначала заметим, что управляемая динамическая память является не просто случайным фрагментом памяти, доступной для среды выполнения. Сборщик мусора .NET является исключительно аккуратным "дворником" в динамической памяти – он (при необходимости) даже сжимает пустые блоки памяти с целью оптимизации. Чтобы упростить задачу сборки мусора, управляемая динамическая память имеет указатель (обычно называемый указателем на следующий объект, или указателем на новый объект),который идентифицирует точное место размещения следующего объекта.
Инструкция newobj информирует среду CLR о том, что необходимо выполнить следующие главные задачи.
• Вычислить общий объем памяти, необходимой для размещения объекта (включая память, необходимую для членов-переменных и базовых классов типа).
• Проверить управляемую динамическую память, чтобы гарантировать достаточный объем памяти для размещаемого объекта. Если памяти достаточно, вызывается конструктор типа, и вызывающей стороне, в конечном счете, возвращается ссылка на новый объект в памяти, причем адрес этого объекта будет соответствовать последней позиции указателя на следующий объект.
• Наконец, перед возвращением ссылки вызывающей стороне необходимо изменить значение указателя на следующий объект, чтобы указатель соответствовал началу свободной области управляемой динамической памяти.
Этот процесс схематически показан на риc. 5.2.
Рис. 5.2. Размещение объектов в управляемой динамической памяти
При размещении объектов в приложении пространство управляемой динамической памяти может, в конечном счете, заполниться. Если при обработке инструкции newobj среда CLR обнаружит, что управляемой динамической памяти недостаточно для размещения очередного типа, с целью освобождения памяти будет выполнена сборка мусора. Поэтому следующее правило сборки мусора также оказывается очень простым.
• Правило. Если управляемая динамическая память недостаточна дли размещения очередного объекта, выполняется сборка мусора.
В процессе сборки мусора сборщик мусора временно приостанавливает все активные потоки в рамках текущего процесса, чтобы гарантировать, что приложение не получит доступа к динамической памяти в процессе сборки мусора. Тему потоков мы рассмотрим в главе 14, а пока что воспринимайте поток, как "нить" выполнения в пределах выполняющейся программы. После завершения цикла сборки мусора приостановленным потокам будет разрешено продолжить работу. К счастью, сборщик мусора .NET хорошо оптимизирован, и вы вряд ли заметите соответствующие короткие перерывы в работе вашего приложения.