Автор: Sergey Teplyakov

Никогда не задумывались о том, что значит булевый флаг trackResuraction в конструкторе WeakReference и что же такое short weak reference и long weak reference? Есть шансы, что вы не задумывались об этом просто потому, что никогда этих зверей не использовали в своем коде. Я тоже не особо задумывался об этой разнице, но вот, давеча, решил разобраться с этими вещами более подробно.

ПРИМЕЧАНИЕ
Разница между short и long weak references существует лишь при работе с финализируемыми объектами. Выходит, что это весьма специфическая тема, но ее рассмотрение позволит чуть глубже разобраться с внутренним устройством и поведением сборщика мусора.

Итак, давайте вспомним, что происходит при создании объекта А, содержащего финализатор:

  1. Выделяется память в управляемой куче.
  2. Если объект реализует финализатор (в нашем случае это так), то указатель на него кладется в очередь для финализации (finalization queue).
  3. Вызывается конструктор объекта А.

После чего ссылка на вновь созданный объект сохраняется в локальной переменной или поле объекта:

image

Предположим, что в некоторый момент времени на вновь созданный объект больше не остается ссылок из корней приложения (application roots), при этом ссылка из finalization queue не рассматривается в качестве корневой. Тогда во время ближайшей сборки мусора объект считается достижимым для сборки, но прежде чем расправиться с ним окончательно, GC все таки проверяет наличие ссылки на него в finalization queue.

Поскольку ссылка на объект А находится в finalization queue, то вместо удаления объекта ссылка на него сохраняется в другой очереди: в очереди объектов, готовых для финализации (freachable queue). Поскольку объект выживает после первой сборки мусора, поэтому он переходит в первое поколение объектов.

image

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

Чтобы полностью избавиться от объекта А, вначале исполняется его финализатор (в отдельном потоке), после чего объект удаляется из freachable queue. И лишь после этого, во время следующей сборки мусора объект А окончательно может быть удален.

А при чем здесь слабые ссылки?

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

Теперь должно быть не сложно догадаться, для чего нужен флаг trackResurection у конструктора WeakReference. По умолчанию (trackResurection false), слабая ссылка является короткой (short weak reference), при этом Target этой ссылки сбрасывается в null во время первого этапа сборки мусора, т.е. когда на объект пропадают ссылки из кода приложения. А для длинной слабой ссылки (long weak reference) во время второго!

Вот небольшой пример, который демонстрирует разницу в поведении:// Кастомный объект с финализатором
class Finalizable
{
~Finalizable()
{
Console.WriteLine('Finalizable.dtor'),
}
}
// Простой трекер, который генерирует событие, когда объект
// на который указывает слабая ссылка умирает
class WeakReferenceTracker
{
private readonly WeakReference _wr,
public WeakReferenceTracker(object o, bool trackResurection)
{
_wr =
new WeakReference(o, trackResurection),
// Начинаем следить за тем, когда объект умрет!
Task
.Factory.StartNew(TrackDeath),
}
public event Action ReferenceDied = () =>, { },
// Не слишком надежная реализация, но для наших целей вполне подходящая
private void
TrackDeath()
{
while (true)
{
if (!_wr.IsAlive)
{
ReferenceDied(),
break,
}
Thread.Sleep(1),
}
}
}

И теперь достаточно создать два разных трекера для одного финализируемого объекта и посмотреть порядок обнуления слабых ссылок:public static void Main()
{
Console.WriteLine('Creating 2 trackers...'),
var finalizable = new Finalizable(),
var weakTracker = new WeakReferenceTracker(finalizable, false),
weakTracker.ReferenceDied +=
() =>,
Console.WriteLine('Short weak reference is dead'),
var resurectionTracker = new WeakReferenceTracker(finalizable, true),
resurectionTracker.ReferenceDied +=
() =>,
Console.WriteLine('Long weak reference is dead'),
Console.WriteLine('Forcing 0th generation GC...'),
GC.Collect(0),
Thread.Sleep(100),
Console.WriteLine('Forcing 1th generation GC...'),
GC.Collect(1),
// Это предотвратит уничтожение сборщиком мусора самих трекеров
GC
.KeepAlive(weakTracker),
GC.KeepAlive(resurectionTracker),
Console.ReadLine(),
}

Запускаем это чудо без отладки и получаем следующий вывод:

Creating 2 trackers...
Forcing 0th generation GC...
Finalizable.dtor
Short weak reference is dead
Forcing 1th generation GC...
Long weak reference is dead

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

Исходя из этого вывода на экран мы видим, что short weak reference умирает после того, как объект стал недостижим из кода приложения, а long weak reference умирает лишь после окончательного удаления объекта во время следующей сборки мусора.

Дополнительные статьи о сборке мусора

  • Немного о сборке мусора и поколениях
  • Dispose Pattern
  • О явном вызове метода Dispose
  • Источники о сборке мусора в .NET

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

You have no rights to post comments

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

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