Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
Базовые сведения о времени жизни объектов
При создании приложений C# корректно допускать, что исполняющая среда .NET Core позаботится об управляемой куче без вашего прямого вмешательства. В действительности "золотое правило" по управлению памятью в .NET Core выглядит простым.
Правило. Используя ключевое слово new, поместите экземпляр класса в управляемую кучу и забудьте о нем.
После создания объект будет автоматически удален сборщиком мусора, когда необходимость в нем отпадет. Конечно, возникает вполне закономерный вопрос о том, каким образом сборщик мусора выясняет, что объект больше не нужен? Краткий (т.е. неполный) ответ можно сформулировать так: сборщик мусора удаляет объект из кучи, только когда он становится недостижимым для любой части кодовой базы. Добавьте в класс Program метод, который размещает в памяти локальный объект Car:
static void MakeACar()
{
// Если myCar - единственная ссылка на объект Car, то после
// завершения этого метода объект Car *может* быть уничтожен.
Car myCar = new Car();
}
Обратите внимание, что ссылка на объект Car(myCar) была создана непосредственно внутри метода MakeACar() и не передавалась за пределы определяющей области видимости (через возвращаемое значение или параметр ref/out). Таким образом, после завершения данного метода ссылка myCar оказывается недостижимой, и объект Car теперь является кандидатом на удаление сборщиком мусора. Тем не менее, важно понимать, что восстановление занимаемой этим объектом памяти немедленно после завершения метода MakeACar() гарантировать нельзя. В данный момент можно предполагать лишь то, что когда исполняющая среда инициирует следующую сборку мусора, объект myCar может быть безопасно уничтожен.
Как вы наверняка сочтете, программирование в среде со сборкой мусора значительно облегчает разработку приложений. И напротив, программистам на языке C++ хорошо известно, что если они не позаботятся о ручном удалении размещенных в куче объектов, тогда утечки памяти не заставят себя долго ждать. На самом деле отслеживание утечек памяти — один из требующих самых больших затрат времени (и утомительных) аспектов программирования в неуправляемых средах. За счет того, что сборщику мусора разрешено взять на себя заботу об уничтожении объектов, обязанности по управлению памятью перекладываются с программистов на исполняющую среду.
Код CIL для ключевого слова new
Когда компилятор C# сталкивается с ключевым словом new, он вставляет в реализацию метода инструкцию newobj языка CIL. Если вы скомпилируете текущий пример кода и заглянете в полученную сборку с помощью утилиты ildasm.ехе, то найдете внутри метода MakeACar() следующие операторы CIL:
.method assembly hidebysig static
void '<<Main>$>g__MakeACar|0_0'() cil managed
{
// Code size 8 (0x8)
.maxstack 1
.locals init (class SimpleGC.Car V_0)
IL_0000: nop
IL_0001: newobj instance void SimpleGC.Car::.ctor()
IL_0006: stloc.0
IL_0007: ret
} // end of method '<Program>$'::'<<Main>$>g__MakeACar|0_0'
Прежде чем ознакомиться с точными правилами, которые определяют момент, когда объект должен удаляться из управляемой кучи, давайте более подробно рассмотрим роль инструкции newobj языка CIL. Первым делом важно понимать, что управляемая куча представляет собой нечто большее, чем просто произвольную область памяти, к которой исполняющая среда имеет доступ. Сборщик мусора .NET Core "убирает" кучу довольно тщательно, при необходимости даже сжимая пустые блоки памяти в целях оптимизации.
Для содействия его усилиям в управляемой куче поддерживается указатель (обычно называемый указателем на следующий объект или указателем на новый объект), который идентифицирует точное местоположение, куда будет помещен следующий объект. Таким образом, инструкция newobj заставляет исполняющую среду выполнить перечисленные ниже основные операции.
1. Подсчитать общий объем памяти, требуемой для размещения объекта (в том числе память, необходимую для членов данных и базовых классов).
2. Выяснить, действительно ли в управляемой куче имеется достаточно пространства для сохранения размещаемого объекта. Если места хватает, то указанный конструктор вызывается, и вызывающий код в конечном итоге получает ссылку на новый объект в памяти, адрес которого совпадает с последней позицией указателя на следующий объект.
3. Наконец, перед возвращением ссылки вызывающему коду переместить указатель на следующий объект, чтобы он указывал на следующую доступную область в управляемой куче.
Описанный процесс проиллюстрирован на рис. 9.2.
В результате интенсивного размещения объектов приложением пространство внутри управляемой кучи может со временем заполниться. Если при обработке инструкции newobj исполняющая среда определяет, что в управляемой куче недостаточно места для размещения объекта запрашиваемого типа, тогда она выполнит сборку мусора, пытаясь освободить память. Соответственно, следующее правило сборки мусора выглядит тоже довольно простым.
Правило. Если в управляемой куче не хватает пространства для размещения требуемого объекта, то произойдет сборка мусора.
Однако то, как конкретно происходит сборка мусора, зависит от типа сборки мусора, используемого приложением. Различия будут описаны далее в главе.
Установка объектных ссылок в null
Программисты на C/C++ часто устанавливают переменные указателей в null, гарантируя тем самым, что они больше не ссылаются на какие-то местоположения в неуправляемой памяти. Учитывая такой факт, вас может интересовать, что происходит в результате установки в null ссылок на объекты в С#. В качестве примера измените метод MakeACar() следующим образом:
static void MakeACar()
{
Car myCar = new Car();
myCar = null;
}
Когда ссылке на объект присваивается null, компилятор C# генерирует код CIL, который гарантирует, что ссылка (myCar в данном примере) больше не указывает на какой-либо объект. Если теперь снова с помощью утилиты ildasm.exe просмотреть код CIL модифицированного метода MakeACar(), то можно обнаружить в нем код операции ldnull (заталкивает значение null в виртуальный стек выполнения), за которым следует код операции stloc.0 (устанавливает для переменной ссылку null):
.method assembly hidebysig static
void '<<Main>$>g__MakeACar|0_0'() cil managed
{
// Code size 10 (0xa)
.maxstack 1
.locals init (class SimpleGC.Car V_0)
IL_0000: nop
IL_0001: newobj instance void SimpleGC.Car::.ctor()
IL_0006: stloc.0
IL_0007: ldnull
IL_0008: stloc.0
IL_0009: