Автор: Sergey Teplyakov

DISCLAIMER: это относительно продвинутая статья о сборке мусора, поэтому автор предполагает минимальное знакомство читателя с принципом работы сборщика мусора CLR.

Вопрос: может ли объект стать достижимым для сборки мусора до окончания вызова конструктора?

Поскольку объект не может контролировать процесс своего уничтожения, то этот вопрос можно перефразировать так: может ли финализатор вызваться до окончания вызова конструктора?

Но вместо того, чтобы сразу отвечать на эти вопросы, давайте рассмотрим основные особенности сборки мусора и ответим на более простые вопросы.

Может ли локальная переменная стать 'мусором' до окончания работы метода?

Давайте рассмотрим такой пример:public static void Main()
{
var foo = new Foo(),
foo.DoSomething(),
Console.ReadLine(),
}

Когда в этом случае переменная foo становится достижимой для сборки мусора? По окончанию выполнения метода Main или начиная с третьей строки? Ответ на этот вопрос зависит от реализации CLR, но для 'десктопной' CLR ответ такой: переменная foo становится 'мусором' сразу после того, как она перестает использоваться, т.е. с третьей строки метода Main.

ПРИМЕЧАНИЕ
Такое поведение проявляется лишь в релизных сборках и не проявляется при подключенном отладчике. При подключенном отладчике время жизни 'продлевается' до окнчания метода, что дает возможность увидеть значение переменных в любой точке метода.

Все дело в том, как именно CLR определяет, является ли локальная переменная действующим корнем (root) или нет. Наиболее простой вариант реализации может рассматривать все локальные переменные (или аргументы метода) в качестве корней на протяжении всего метода. Но текущая реализация CLR поддерживает 'eager root collection', что позволяет определить достижимость объектов более эффективным способом.

Так, при JIT компиляции метода Main, JIT проанализирует этот метод и создаст специальную табличку, в которой будет говориться, в каких диапазонах используется локальная переменная foo. И затем, если сборка мусора произойдет в строке 3, то CLR посмотрит в эту табличку и поймет, что переменная foo уже не используется и не является больше действующим корнем приложения.

ПРИМЕЧАНИЕ
Согласно спецификации языка C#, эта возможность не является обязательной (раздел 3.9): 'if a local variable that is in scope is the only existing reference to an object, but that local variable is never referred to in any possible continuation of execution from the current execution point in the procedure, the garbage collector may (but is not required to) treat the object as no longer in use'.
Так, например, данная возможность не реализована в CLR под Windows Phone!

Может ли объект стать 'мусором' при выполнении экземплярного метода?

Теперь давайте усложним вопрос: может ли объект быть собран сборщиком мусора при выполнении экземплярного метода?

На первый взгляд это кажется невозможным, но это не так. Экземплярный метод можно рассматривать, как обычный статический метод, первым аргументом которого передается this. И если этот 'this' не используется на протяжении метода (нет обращения к полям объекта), и на этот объект не остается других ссылок из корней приложения, то данный объект может быть собран сборщиком мусора до окончания выполнения экземплярного метода.

Вот простой код, демонстрирующий эту возможность:

ПРИМЕЧАНИЕ
Как всегда, любые примеры следует запускать в релизном режиме и без отладки!

internal class Program
{
~Program()
{
Print(
'Program.dtor'),
}
private static void Print(string message)
{
GC.Collect(),
GC.WaitForPendingFinalizers(),
Console.WriteLine(message),
}
private void InstanceMethod()
{
Console.WriteLine('Instance method began'),
Print(
'Instance method finished'),
}
public static void Main()
{
var program = new Program(),
program.InstanceMethod(),
}
}

При запуске мы получим следующий вывод:

Instance method began
Program.dtor
Instance method finished

Ну а теперь мы подошли к нашему исходному вопросу:

Может ли объект стать 'мусором' до окончания вызова конструктора?

Да, легко! Ниже приведет пример кода, демонстрирующий эту возможность:class Racer
{
private readonly int _x,
public Racer(int x)
{
_x = x,
Print(
'ctor', _x),
}
~Racer()
{
Print(
'dtor', _x),
}
public static void Print(string message, int objectId)
{
GC.Collect(),
GC.WaitForPendingFinalizers(),
Console.WriteLine('{0}, object id: {1}', message, objectId),
}
}
public static void
Main()
{
var racer = new Racer(42),
Console.ReadLine(),
}

Мы гарантированно получим:

dtor, object id: 42
ctor, object id: 42

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

  1. Выделение памяти в управляемой куче.
  2. Добавление указатель на вновь созданный объект в очередь для финализации.
  3. Вызвать конструктор объекта.

Таким образом, объект добавляется в очередь для финализации до вызова конструктора, что дает гарантию вызова финализатора даже если конструктор сгенерирует исключение. А раз так, то CLR вполне может посчитать вновь созданный объект недостижимым даже если конструктор объекта еще не завершен!

В третьих, может показаться, что конструктор объекта использует this до конца метода, но это не так. На самом деле, поле _x передается в метод Print по значению, а значит уже во время вызова метода Print this больше не используется!

Заключение

На самом деле, рассматриваемые сюрпризы довольно маловероятны в реальном коде. Вероятность этого низка просто потому, что мы очень редко создаем самостоятельно финализируемые объекты. Да и возможна эта ситуация если конструктор выполняет длительную операцию с побочными эффектами без обращения к собственным полям.

С другой стороны, если мы завязаны на побочные эффекты и, например, логируем куда-то во внешнее хранилище время создания и уничтожения экземпляра, то в этом случае описанное поведение вполне возможно.

Дополнительные ссылки

Эрик Липперт поднимал этот вопрос пару месяцев назад в своей статье 'Construction Destruction', но не многие знают, что подобную проблему поднимал на rsdn-е nikov еще в 2006-м году в обсуждении 'race condition между Finalize и Dispose'. Так что это, в некотором роде, баян, но очень уж любопытный!

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).