Обновляются ли ссылки, когда сборщики мусора перемещают данные в кучу?

Я читал, что GC (Garbage Collectors) перемещает данные в Heap по соображениям производительности, что я не совсем понимаю, почему, поскольку это оперативная память, возможно, для лучшего последовательного доступа, но мне интересно, если ссылки в Stack обновляются, когда происходит такое движение в куче. Но, возможно, адрес смещения остается тем же, но другие части данных перемещаются сборщиками мусора, но я не уверен.

Я думаю, что этот вопрос относится к деталям реализации, поскольку не все сборщики мусора могут выполнять такую оптимизацию, или они могут это делать, но не обновлять ссылки (если это обычная практика среди реализаций сборщика мусора). Но я хотел бы получить общий ответ, специфичный для сборщиков мусора CLR (Common Language Runtime).

А также я читал статью Эрика Липперта "Ссылки не адреса" здесь, и следующий абзац немного смутил меня:

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

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

А также, если какое-либо из моих предположений неправильно, пожалуйста, исправьте меня.

Ответ 1

Да, ссылки обновляются во время сбора мусора. При необходимости объекты перемещаются при уплотнении кучи. Уплотнение служит двум основным целям:

  • это делает программы более эффективными, используя кеширование данных процессора более эффективно. Это очень и очень много для современных процессоров, оперативная память чрезвычайно медленная по сравнению с движком исполнения, толстая на два порядка. Процессор может быть остановлен для сотен инструкций, когда ему нужно дождаться, когда ОЗУ предоставит переменное значение.
  • он решает проблему фрагментации, от которой страдают кучи. Фрагментация происходит при выпуске небольшого объекта, окруженного живыми объектами. Отверстие, которое нельзя использовать ни для чего другого, кроме объекта с равным или меньшим размером. Плохо для эффективности использования памяти и эффективности процессора. Обратите внимание, что LOH, куча больших объектов в.NET, не уплотняется и, следовательно, страдает от этой проблемы фрагментации. Много вопросов об этом в SO.

Несмотря на то, что Eric didactic, ссылка на объект действительно является только адресом. Указатель, точно такой же, какой вы использовали бы в программе C или C++. Очень эффективно, обязательно так. И весь GC должен сделать после перемещения объекта, обновляет адрес, хранящийся в этом указателе на перемещенный объект. CLR также позволяет выделять дескрипторы объектов, дополнительные ссылки. Выставляется как тип GCHandle в.NET, но необходимо только в том случае, если GC нуждается в помощи, определяющей, должен ли объект оставаться в живых или его не следует перемещать. Имеет значение только если вы взаимодействуете с неуправляемым кодом.

Что не так просто - это найти этот указатель. CLR сильно инвестируется в обеспечение того, что можно сделать надежно и эффективно. Такие указатели могут храниться во многих разных местах. Более легкими для поиска являются ссылки на объекты, хранящиеся в поле объекта, статическая переменная или GCHandle. Жесткие - это указатели, хранящиеся в стеке процессора или регистре CPU. Например, для аргументов метода и локальных переменных.

Одна из гарантий, которую CLR должна обеспечить, чтобы это произошло, состоит в том, что GC всегда может надежно пройти стек потока. Таким образом, он может найти локальные переменные обратно, которые хранятся в фрейме стека. Тогда ему нужно знать, где искать такой фрейм стека, что работа компилятора JIT. Когда он компилирует метод, он не просто генерирует машинный код для метода, но также создает таблицу, которая описывает, где хранятся эти указатели. Более подробную информацию об этом вы найдете в этом посте.

Ответ 2

Глядя на C++\CLI В действии, есть раздел о внутренних указателях против указателей пиннинга:

C++/CLI предоставляет два типа указателей, которые работают вокруг этой проблемы. Первый вид называется внутренним указателем, который обновляется средой выполнения, чтобы отражать новое местоположение объекта, указывающее на каждый раз, когда объект перемещается. Физический адрес, на который указывает внутренний указатель, никогда не остается прежним, но он всегда указывает на тот же объект. Другой вид называется указателем пиннинга, который не позволяет GC переместить объект; другими словами, он связывает объект с определенным физическим местоположением в куче CLR. С некоторыми ограничениями возможны преобразования между внутренними, пиннинг и внутренними указателями.

Из этого вы можете сделать вывод, что ссылочные типы перемещаются в кучу, и их адреса меняются. После фазы Mark и Sweep объекты уплотняются внутри кучи, тем самым фактически переходя к новым адресам. CLR отвечает за отслеживание фактического местоположения хранилища и обновление этих внутренних указателей с использованием внутренней таблицы, следя за тем, чтобы при доступе он все же указывал на правильное местоположение объекта.

Вот пример, взятый отсюда:

ref struct CData
{
    int age;
};

int main()
{
    for(int i=0; i<100000; i++) // ((1))
        gcnew CData();

    CData^ d = gcnew CData();
    d->age = 100;

    interior_ptr<int> pint = &d->age; // ((2))

    printf("%p %d\r\n",pint,*pint);

    for(int i=0; i<100000; i++) // ((3))
        gcnew CData();

    printf("%p %d\r\n",pint,*pint); // ((4))
    return 0;
}

Это объясняется:

В образце кода вы создаете 100 000 объектов-сирот CData ((1)), чтобы вы могли заполнить хорошую часть кучи CLR. Затем вы создаете объект CData, который хранится в переменной, и ((2)) внутренний указатель на возраст члена этого объекта CData. Затем вы распечатываете адрес указателя, а также значение int, на которое указывает. Теперь ((3)) вы создаете еще 100 000 объектов-сирот-CData; где-то вдоль линии происходит цикл сбора мусора (объекты-сироты, созданные ранее ((1)), собираются, потому что они нигде не упоминаются). Обратите внимание, что вы не используете вызов GC :: Collect, потому что это не гарантирует принудительный цикл сбора мусора. Как вы уже видели в обсуждении алгоритма сбора мусора в предыдущей главе, GC освобождает пространство путем удаления объектов-сирот, чтобы он мог выполнять дальнейшие распределения. В конце кода (к тому времени произошла сборка мусора), вы снова ((4)) распечатаете адрес указателя и значение возраста. Это результат, который я получил на своей машине (обратите внимание, что адреса будут отличаться от машины к машине, поэтому ваши выходные значения не будут одинаковыми):

012CB4C8 100
012A13D0 100