В .Net GC реализует алгоритм "Пометить, Освободить, Упаковать" описанный например
здесь. Изначально GC рассматривает все объекты как недостижимые. В процессе прохода по графу он помечает все достижимые объекты флажком, а все оставшиеся, соотвтетственно, подлежат сборке. Такой алгоритм осуществляется в один проход и позволяет легко определять и уничтожать "острова изоляции", а также не подвержен проблеме зацикленных ссылок.
Значительный интерес представляет то, как GC поступает с объектами, имеющими файналайзеры. Можно ли из файналайзера обращаться к другим reference type объектам? Могут ли эти reference type объекты оказаться уже уничтоженными и что тогда произойдет?
Давайте определим термины:
завершенными мы будем называть объекты для которых были вызваны их файналайзеры.
уничтоженными или собранными мы будем называть те объекты, память занимаемая которыми была освобождена и возвращена куче.
Сразу же скажу, что, в отличии от C++, обратиться к уничтоженным объектам в .Net
н е в о з м о ж н о. Зато обращение к завершенным объектам вполне допустимо, более того, они даже могут быть "воскрешены".
На нижеприведенной диаграмме приблизительно описан жизненный цикл reference type объектов:
Из диаграммы сразу видно, что объекты имеющие файналайзеры живут на один цикл дольше обычных и, по всей видимости, не могут быть собраны с поколением 0 (хотя это зависит от деталей реализации GC).
Условные обозначения на диаграмме:
Finalization Queue - очередь финализации (терминология из MSDN)
Freachable Queue - согласно терминологии MSDN, это список объектов готовых к завершению (финализации). Объекты попавшие в этот список, а так же все объекты на которые они ссылаются и так далее, не подлежат уничтожению и переживают сборку мусора. Поэтому из файналайзера можно смело обращаться к любым объектам, все они живы, другое дело что они могут оказаться уже завершенными.
Рассмотрим следующую ситуацию:
class ClassA
{
bool _disposed;
public void DoSomething()
{
if (_disposed)
throw new ObjectDisposedException("ClassA");
}
~ClassA()
{
_disposed = true;
}
} //class
class ClassB
{
ClassA _classA;
public ClassB()
{
_classA = new ClassA();
}
~ClassB()
{
_classA.DoSomething();
}
} //class
* This source code was highlighted with Source Code Highlighter.
К моменту когда ClassB в файналайзере обращается к ClassA.DoSomething экземпляр ClassA может оказаться уже завершенным и в этом случае метод DoSomething кинет ObjectDisposedException. Если исключение корректно обработается в вызывающем коде, то никаких проблем нет. Если же оно "пролетит мимо" то это может привести к аварийному завершению всего приложения согласно
тексту из MSDN. Вот почему в файналазерах необходимо осторожно работать с обращениями к другим reference type объектам.
Теперь об оживлении.
В файналайзере объект имеет право передать другому, в том числе и живому объекту ссылку на самого себя. В этом случае он и все объекты на которые он ссылается вновь окажутся доступными для кода программы и как бы воскреснут. Правда восрешенный объект не будет больше состоять в Finalization Queue и когда он следующий раз окажется недоступным, его файналайзер больше не будет вызван и объект окажется просто уничтоженным. Для того, чтоб вновь поставить объект в очередь финализации нужно воспользоваться вызовом
GC.ReRegisterForFinalize
Хотя лично мне подобный трюк не нравится потому, что он, например, может привести к появлению бессмертных Дунканов Маклаудов, утечкам памяти и излижним нагрузкам на GC.
Ниже приведен пример кода в котором объект восстанавливается в файналайзере:
class RecoverableObject
{
int _id;
static int Counter;
bool _canDie;
public static RecoverableObject Root;
static RecoverableObject()
{
Root = new RecoverableObject();
Root.CanDie = true;
}
public RecoverableObject()
{
_id = ++Counter;
}
~RecoverableObject()
{
if (CanDie)
{
Console.WriteLine("good bye Id={0}", _id);
return;
}
Console.WriteLine("obj.Id={0} is going to be recovered", _id);
Root.Other = this;
GC.ReRegisterForFinalize(this);
}
public int Id
{
get
{
return _id;
}
}
public bool CanDie
{
get
{
return _canDie || this == Root;
}
set
{
_canDie = value;
}
}
public RecoverableObject Other { get; set; }
static void RecoverableObjectTest()
{
RecoverableObject obj;
obj = new RecoverableObject();
Console.WriteLine("first collection...");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("after the first collection");
Console.WriteLine();
obj = RecoverableObject.Root.Other;
RecoverableObject.Root.Other = null;
Console.WriteLine("obj.Id={0} is available again", obj.Id);
Console.WriteLine("second collection...");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("after the second collection");
Console.WriteLine();
obj = RecoverableObject.Root.Other;
RecoverableObject.Root.Other = null;
Console.WriteLine("obj.Id={0} is available again", obj.Id);
obj.CanDie = true;
Console.WriteLine("third collection, obj.Id={0} must die!", obj.Id);
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("after the third collection");
Console.WriteLine();
Console.WriteLine("RecoverableObjectTest exiting...");
}
} //class
* This source code was highlighted with Source Code Highlighter.
Вкратце, вот что нужно знать о финализации:
- Все объекты, имеющие метод Finalize помещаются в очередь финализации при их создании. Уничтожение объектов помещенных в очередь финализации производится как минимум в два этапа (может быть и больше, если их оживлять)
- Метод Finalize не должен выдавать исключения, поскольку они не могут обрабатываться приложением и могут привести к завершению работы приложения.
- Реализация методов Finalize (деструкторов) может отрицательно воздействовать на производительность, и злоупотреблять ими не рекомендуется.
Данный пост отвечает на вопрос, поставленный в посте "Qyoto, Issues for investigation" о том правомерно ли обращаться из файналайзера к reference type объектам. Отвечает утвердительно: "Да, правомерно"
В ближайших статьях я постараюсь дать ответы на оставшиеся вопросы.