Как сборщик мусора С# находит объекты, единственная ссылка которых является указателем на интерьер?

В параметрах С#, ref и out, насколько мне известно, передаются, передавая только исходный адрес соответствующего значения. Этот адрес может быть внутренним указателем на элемент в массиве или поле внутри объекта.

Если происходит сбор мусора, возможно, что единственная ссылка на какой-либо объект осуществляется через один из этих внутренних указателей, как в:

using System;

public class Foo
{
    public int field;

    public static void Increment(ref int x) {
        System.GC.Collect();
        x = x + 1;
        Console.WriteLine(x);
    }

    public static void Main()
    {
        Increment(ref new Foo().field);
    }
}

В этом случае GC должен найти начало объекта и обработать весь объект как достижимый. Как оно это делает? Нужно ли сканировать всю кучу в поисках объекта, содержащего этот указатель? Это кажется медленным.

Ответ 1

У сборщика мусора будет быстрый способ найти начало объекта из управляемого указателя внутреннего интерьера. Оттуда он может, очевидно, пометить объект как "привязанный" при выполнении фазы подметания.

У вас нет кода для коллекционера Microsoft, но они будут использовать что-то похожее на таблицу Go span, которая быстро просматривает разные "промежутки" памяти, которые вы можете использовать для наиболее значимых X-бит указателя в зависимости от насколько велики вы выбираете промежутки. Оттуда они используют тот факт, что каждый пролет содержит X число объектов одного размера, чтобы очень быстро найти заголовок того, который у вас есть. Это в значительной степени операция O (1). Очевидно, что куча Microsoft будет отличаться, поскольку она распределяется последовательно без учета размера объекта, но у них будет какая-то структура поиска O (1).

https://github.com/puppeh/gcc-6502/blob/master/libgo/runtime/mgc0.c

// Otherwise consult span table to find beginning.
// (Manually inlined copy of MHeap_LookupMaybe.)
k = (uintptr)obj>>PageShift;
x = k;
x -= (uintptr)runtime_mheap.arena_start>>PageShift;
s = runtime_mheap.spans[x];
if(s == nil || k < s->start || (const byte*)obj >= s->limit || s->state != MSpanInUse)
    return false;
p = (byte*)((uintptr)s->start<<PageShift);
if(s->sizeclass == 0) {
    obj = p;
} else {
    uintptr size = s->elemsize;
    int32 i = ((const byte*)obj - p)/size;
    obj = p+i*size;
}

Обратите внимание, что сборщик мусора .NET - это копировальный коллектор, поэтому управляемые/внутренние указатели должны обновляться всякий раз, когда объект перемещается во время цикла сбора мусора. GC будет знать, где внутри внутренних указателей стека для каждого кадра стека, основываясь на параметрах метода, известных во время JIT.

Ответ 2

Ваш код компилируется в

    IL_0001: newobj instance void Foo::.ctor()
    IL_0006: ldflda int32 Foo::'field'
    IL_000b: call void Foo::Increment(int32&)

AFAIK, команда ldflda создает ссылку на объект, содержащий это поле, до тех пор, пока адрес находится в стеке (до завершения call).

Ответ 3

Сборщик мусора выполняет три основных этапа:

  • Отметьте все объекты, которые все еще живы.
  • Соберите объекты, которые не помечены как живые.
  • Компактная память.

Ваша забота - это шаг 1: Как GC выясняет, что он не должен собирать объекты за ref и out params?

Когда GC выполняет сбор, он начинается с состояния, в котором ни один из объектов не считается живым. Затем он переходит от корневых ссылок и отмечает все эти объекты как живые. Корневые ссылки - это все ссылки на стек и в статических полях. Затем GC идет рекурсивно в отмеченные объекты и маркирует все объекты как живые, на которые ссылаются. Это повторяется до тех пор, пока не будут найдены никакие объекты, которые еще не отмечены как живые. Результатом этой операции является граф объектов.

A ref или out имеет ссылку на стек, поэтому GC будет отмечать соответствующий объект как живой, потому что стек является корнем для графа объектов.

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

В конце, GC переместит все живые объекты в непрерывную область памяти в начале кучи. Остальная часть памяти будет заполнена нулями. Это упрощает процесс создания новых объектов, поскольку их память всегда может быть выделена в конце кучи, и все поля уже имеют значения по умолчанию.

Это правда, что GC требуется некоторое время, чтобы сделать все это, но он все еще делает это достаточно быстро, из-за некоторых оптимизаций. Одна из оптимизаций состоит в том, чтобы разделить кучу на поколения. Все вновь выделенные объекты - это поколение 0. Все объекты, пережившие первую коллекцию, - это поколение 1 и т.д. Высшие поколения собираются только тогда, когда сбор низших поколений не освобождает достаточное количество памяти. Итак, нет, GC не всегда должен сканировать всю кучу.

Вы должны учитывать, что, хотя коллекция занимает некоторое время, выделение новых объектов (что происходит гораздо чаще, чем сборка мусора) происходит намного быстрее, чем в других реализациях, где куча больше похожа на швейцарский сыр, и вам нужно некоторое время, чтобы найти отверстие, достаточно большое для нового объекта (который вам еще нужно инициализировать).