воскресенье, 9 января 2011 г.

Finalizer, Как работает GC?

В .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.

Вкратце, вот что нужно знать о финализации:
  1. Все объекты, имеющие метод Finalize помещаются в очередь финализации при их создании. Уничтожение объектов помещенных в очередь финализации производится как минимум в два этапа (может быть и больше, если их оживлять)
  2. Метод Finalize не должен выдавать исключения, поскольку они не могут обрабатываться приложением и могут привести к завершению работы приложения.
  3. Реализация методов Finalize (деструкторов) может отрицательно воздействовать на производительность, и злоупотреблять ими не рекомендуется.


Данный пост отвечает на вопрос, поставленный в посте "Qyoto, Issues for investigation" о том правомерно ли обращаться из файналайзера к reference type объектам. Отвечает утвердительно: "Да, правомерно"

В ближайших статьях я постараюсь дать ответы на оставшиеся вопросы.

Комментариев нет:

Отправить комментарий