Базовые сведения о времени жизни объектов

52

Программистам на C# никогда не приходится непосредственно удалять управляемый объект из памяти (в языке C# нет даже ключевого слова вроде delete). Вместо этого объекты .NET размещаются в области памяти, которая называется управляемой кучей (managed heap), откуда они автоматически удаляются сборщиком мусора, когда наступает "определенный момент в будущем".

Давайте проясним различие между классами, объектами и ссылками. Вспомните, что класс представляет собой ни что иное, как схему, которая описывает то, каким образом экземпляр данного типа должен выглядеть и вести себя в памяти. Определяются классы в файлах кода (которым по соглашению назначается расширение *.cs). Для примера создадим новый проект типа Console Application (Консольное приложение) на C# и определим в нем следующий простой класс UserInfo:

using System;

namespace ConsoleApplication1
{
    class UserInfo
    {
        public string Name { get; set; }
        public byte Age { get; set; }

        public UserInfo() { }
        public UserInfo(string Name, byte Age)
        {
            this.Name = Name;
            this.Age = Age;
        }

        public override string ToString()
        {
            return String.Format(@"Имя пользователя: {0}
Возраст: {1}",Name,Age);
        }
    }

    class Program
    {
        static void Main()
        {
            // Создание нового объекта UserInfo в управляемой куче
            // Возвращаемая ссылка на этот объект user1:
            UserInfo User1 = new UserInfo("Alex", 26);
            Console.WriteLine(User1.ToString());
            Console.ReadLine();
        }
        
        public static void MyUser()
        {
            UserInfo ui = new UserInfo();
        }
    }
}

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

На следующей схеме показаны отношения между классами, объектами и ссылками на них:

Ссылки на объект в управляемой куче C#

При создании приложений на C# можно смело полагать, что исполняющая среда .NET будет сама заботиться об управляемой куче без непосредственного вмешательства со стороны программиста. На самом деле "золотое правило" по управлению памятью в .NET звучит просто:

Размещайте объект в управляемой куче с использованием ключевого слова new и забывайте об этом.

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

Как станет ясно со временем, программирование в среде с автоматической сборкой мусора значительно облегчает разработку приложений. Программистам на С++ хорошо известно, что если они специально не позаботятся об удалении размещаемых в куче объектов, вскоре обязательно начнут возникать "утечки памяти". На самом деле отслеживание проблем, связанных с утечкой памяти, является одним из самых длительных (и утомительных) аспектов программирования в неуправляемых средах. Благодаря назначению ответственным за уничтожение объектов сборщика мусора, обязанности по управлению памятью, по сути, сняты с плеч программиста и возложены на CLR-среду.

CIL-код, генерируемый для ключевого слова new

При обнаружении ключевого слова new компилятор C# вставляет в реализацию метода CIL-инструкцию newobj. Если скомпилировать текущий пример кода и заглянуть в полученную сборку с помощью утилиты ildasm.exe, то можно обнаружить внутри метода MyUser() следующие CIL-операторы:

Вспомогательный CIL-код

Прежде чем ознакомиться с точными правилами, которые определяют момент, когда объект должен удаляться из управляемой кучи, давайте более подробно рассмотрим роль CIL-инструкции newobj. Для начала важно понять, что управляемая куча представляет собой нечто большее, чем просто случайный фрагмент памяти, к которому CLR получает доступ. Сборщик мусора .NET "убирает" кучу довольно тщательно, причем (при необходимости) даже сжимает пустые блоки памяти с целью оптимизации. Чтобы ему было легче это делать, в управляемой куче поддерживается указатель (обычно называемый указателем на следующий объект или указателем на новый объект), который показывает, где точно будет размещаться следующий объект.

Таким образом, инструкция newobj заставляет CLR-среду выполнить перечисленные ниже ключевые операции:

Весь описанный процесс схематично изображен на следующем рисунке:

Детали размещения объектов в управляемой куче

Из-за постоянного размещения объектов приложением пространство в управляемой куче может со временем заполниться. В случае если при обработке следующей инструкции newobj среда CLR обнаруживает, что в управляемой куче не хватает пространства для размещения запрашиваемого типа, она приступает к сборке мусора и тем самым пытается освободить хоть сколько-то памяти. Поэтому следующее правило, касающееся сборки мусора, тоже звучит довольно просто:

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

Однако то, каким именно образом начнет выполняться сборка мусора, зависит от версии .NET, под управлением которой функционирует приложение.

Установка объектных ссылок в null

Если ранее приходилось создавать СОМ-объекты в Visual Basic 6.0, то должно быть известно, что по завершении их использования предпочтительнее устанавливать эти ссылки в Nothing. На внутреннем уровне счетчик ссылок на объект СОМ уменьшалось на единицу, и когда он становился равным нулю, объект можно было удалять из памяти. Аналогичным образом программисты на C/C++ часто предпочитают устанавливать для переменных указателей значение null, гарантируя, что они больше не будут ссылаться на неуправляемую память.

Из-за упомянутых фактов, вполне естественно, может возникнуть вопрос о том, что же происходит в C# после установки объектных ссылок в null. Для примера изменим метод MyUser() следующим образом:

public static void MyUser()
{
   UserInfo ui = new UserInfo();
   ui = null;
}

Когда объектные ссылки устанавливаются в null, компилятор C# генерирует CIL-код, который заботится о том, чтобы ссылка больше не ссылалась ни на какой объект. Если теперь снова воспользоваться утилитой ildasm.ехе и заглянуть с ее помощью в CIL-код измененного метода MyUser(), можно обнаружить в нем код операции ldnull (который заталкивает значение null в виртуальный стек выполнения) со следующим за ним кодом операции stloc.O (который присваивает переменной ссылку null):

CIL-код при установке объектных ссылок в null

Однако обязательно следует понять, что установка ссылки в null никоим образом не вынуждает сборщик мусора немедленно приступить к делу и удалить объект из кучи, а просто позволяет явно разорвать связь между ссылкой и объектом, на который она ранее указывала. Благодаря этому, присваивание ссылкам значения null в C# имеет гораздо меньше последствий, чем в других языках на базе С (или VB 6.0), и совершенно точно не будет причинять никакого вреда.

Пройди тесты
Лучший чат для C# программистов