Понимание сборки мусора в .NET

170

Рассмотрим следующий код:

public class Class1
{
    public static int c;
    ~Class1()
    {
        c++;
    }
}

public class Class2
{
    public static void Main()
    {
        {
            var c1=new Class1();
            //c1=null; // If this line is not commented out, at the Console.WriteLine call, it prints 1.
        }
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine(Class1.c); // prints 0
        Console.Read();
    }
}

Теперь, несмотря на то, что переменная c1 в методе main находится вне области видимости и на GC.Collect()нее больше не ссылается ни один другой объект при вызове, почему она там не завершена?

Виктор Мукерджи
источник
8
GC не сразу освобождает экземпляры, когда они выходят за рамки. Он делает это, когда считает необходимым. Вы можете прочитать все о GC здесь: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061
@ user1908061 (Pssst. Ваша ссылка не работает.)
Dragomok

Ответы:

352

Вы попадаете сюда и делаете очень неправильные выводы, потому что используете отладчик. Вам нужно будет запустить свой код так, как он работает на компьютере вашего пользователя. Сначала перейдите к сборке выпуска с помощью диспетчера сборки + конфигурации, измените комбо «Конфигурация активного решения» в верхнем левом углу на «Выпуск». Затем перейдите в Инструменты + Параметры, Отладка, Общие и снимите флажок «Подавить оптимизацию JIT».

Теперь снова запустите вашу программу и возитесь с исходным кодом. Обратите внимание, что дополнительные скобки не имеют никакого эффекта вообще. И обратите внимание, что установка переменной на ноль вообще не имеет значения. Он всегда будет печатать «1». Теперь это работает так, как вы надеялись и ожидали, что это сработает.

Что и объясняет, почему он работает по-другому, когда вы запускаете сборку Debug. Это требует объяснения того, как сборщик мусора обнаруживает локальные переменные и как на это влияет наличие отладчика.

Прежде всего, джиттер выполняет две важные обязанности, когда компилирует IL для метода в машинный код. Первый очень хорошо виден в отладчике, вы можете увидеть машинный код в окне Debug + Windows + Disassembly. Вторая обязанность, однако, совершенно невидима. Он также генерирует таблицу, которая описывает, как используются локальные переменные внутри тела метода. В этой таблице есть запись для каждого аргумента метода и локальная переменная с двумя адресами. Адрес, где переменная будет сначала хранить ссылку на объект. И адрес инструкции машинного кода, где эта переменная больше не используется. Также, хранится ли эта переменная в кадре стека или в регистре процессора.

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

Адрес «больше не используется» в таблице очень важен. Это делает сборщик мусора очень эффективным . Он может собирать ссылку на объект, даже если он используется внутри метода и этот метод еще не завершен. Что является очень распространенным, ваш метод Main (), например, когда-либо прекратит выполнение только перед завершением вашей программы. Ясно, что вы бы не хотели, чтобы какие-либо ссылки на объекты, используемые внутри этого метода Main (), жили в течение всей программы, что привело бы к утечке. Джиттер может использовать таблицу, чтобы обнаружить, что такая локальная переменная больше не нужна, в зависимости от того, как далеко продвинулась программа внутри этого метода Main () до того, как она сделала вызов.

Почти магический метод, связанный с этой таблицей, - это GC.KeepAlive (). Это очень особенный метод, он вообще не генерирует никакого кода. Его единственная обязанность - изменить эту таблицу. Она простираетсявремя жизни локальной переменной, предотвращая сбор мусора в хранимой ссылке. Единственный раз, когда вам нужно его использовать, это не дать GC чрезмерно увлечься сбором ссылки, что может произойти в сценариях взаимодействия, когда ссылка передается на неуправляемый код. Сборщик мусора не может видеть, что такие ссылки используются таким кодом, поскольку он не был скомпилирован джиттером, поэтому у него нет таблицы, в которой указано, где искать ссылку. Передача объекта делегата в неуправляемую функцию, такую ​​как EnumWindows (), является типичным примером того, когда вам нужно использовать GC.KeepAlive ().

Итак, как вы можете заметить из своего примера фрагмента после запуска его в сборке Release, локальные переменные могут быть собраны раньше, до того, как метод завершит выполнение. Еще мощнее, объект может быть собран во время работы одного из его методов, если этот метод больше не ссылается на это . Есть проблема с этим, очень неудобно отлаживать такой метод. Так как вы можете поместить переменную в окно Watch или проверить ее. И он исчезнет во время отладки, если произойдет сборщик мусора. Это было бы очень неприятно, поэтому джиттер знает о подключенном отладчике. Затем он модифицируеттаблица и изменяет «последний использованный» адрес. И меняет его с обычного значения на адрес последней инструкции в методе. Который поддерживает переменную живым, пока метод не вернулся. Что позволяет вам продолжать смотреть его, пока метод не вернется.

Теперь это также объясняет, что вы видели ранее и почему вы задали вопрос. Он печатает «0», потому что вызов GC.Collect не может собрать ссылку. В таблице сказано, что переменная используется после вызова GC.Collect (), вплоть до конца метода. Вынужден сказать это, подключив отладчик и выполнив сборку Debug.

Установка переменной в null теперь имеет эффект, потому что GC будет проверять переменную и больше не будет видеть ссылку. Но убедитесь, что вы не попадете в ловушку, в которую попали многие программисты C #, на самом деле писать этот код было бессмысленно. Не имеет значения, присутствует ли этот оператор при запуске кода в сборке выпуска. Фактически, оптимизатор джиттера удалит это утверждение, поскольку оно никак не повлияет. Поэтому не пишите подобный код, хотя, похоже, он дал эффект.


Последнее замечание по этой теме - вот что доставляет программистам неприятности, которые пишут небольшие программы, чтобы что-то делать с приложением Office. Отладчик обычно выводит их на неверный путь, они хотят, чтобы программа Office выходила по требованию. Надлежащий способ сделать это - вызвать GC.Collect (). Но они обнаружат, что это не работает, когда они отлаживают свое приложение, приводя их в никогда и никогда не приземляясь, вызывая Marshal.ReleaseComObject (). Ручное управление памятью, оно редко работает должным образом, потому что они легко пропустят невидимую ссылку на интерфейс. GC.Collect () на самом деле работает, только не при отладке приложения.

Ганс Пассант
источник
1
Смотрите также мой вопрос, на который Ганс ответил мне приятно. stackoverflow.com/questions/15561025/…
Дейв Най
1
@HansPassant Я только что нашел это удивительное объяснение, которое также отвечает на часть моего вопроса здесь: stackoverflow.com/questions/30529379/… о GC и синхронизации потоков. Один вопрос, который у меня все еще есть: мне интересно, действительно ли GC сжимает и обновляет адреса, которые используются в регистре (хранятся в памяти, пока приостановлено), или просто пропускает их? Процесс, который обновляет регистры после приостановки потока (до возобновления), кажется мне серьезным потоком безопасности, заблокированным ОС.
Атлас
Косвенно да. Поток приостановлен, GC обновляет резервное хранилище для регистров ЦП. Как только поток возобновляет работу, он теперь использует обновленные значения регистра.
Ганс Пассант
1
@HansPassant, я был бы признателен, если бы вы добавили ссылки на некоторые неочевидные детали сборщика мусора CLR, которые вы описали здесь?
denfromufa
Кажется, что при настройке, важным моментом является то, что «Оптимизировать код» ( <Optimize>true</Optimize>в .csproj) включен. Это значение по умолчанию в конфигурации «Release». Но в случае использования пользовательских конфигураций важно знать, что этот параметр важен.
Ноль3
34

[Просто хотел добавить дальше о внутреннем процессе завершения]

Итак, вы создаете объект, и когда объект собирается, Finalizeдолжен быть вызван метод объекта . Но есть еще кое-что для завершения, чем это очень простое предположение.

КРАТКИЕ КОНЦЕПЦИИ ::

  1. Объекты, НЕ реализующие Finalizeметоды, там Память восстанавливается немедленно, если, конечно, они больше не перезапускаются
    кодом приложения

  2. Объекты реализации Finalizeметоды, Концепция / Внедрение Application Roots, Finalization Queue, Freacheable Queueприходит , прежде чем они могут быть восстановлены.

  3. Любой объект считается мусором, если он НЕ подлежит повторной проверке кодом приложения

Предположим, что :: Классы / Объекты A, B, D, G, H НЕ реализуют FinalizeМетод, а C, E, F, I, J реализуют FinalizeМетод.

Когда приложение создает новый объект, новый оператор выделяет память из кучи. Если тип объекта содержит Finalizeметод, то указатель на объект помещается в очередь завершения .

поэтому указатели на объекты C, E, F, I, J добавляются в очередь завершения. Очереди финализации является внутренней структурой данных под контролем сборщика мусора. Каждая запись в очереди указывает на объект, для которого должен быть вызван метод, прежде чем память объекта может быть восстановлена. На рисунке ниже показана куча, содержащая несколько объектов. Некоторые из этих объектов достижимы из корней приложения

Finalizeа некоторые нет. Когда объекты C, E, F, I и J были созданы, .Net Framework обнаруживает, что у этих объектов есть Finalizeметоды, и указатели на эти объекты добавляются в очередь завершения .

введите описание изображения здесь

Когда происходит GC (1-я коллекция), объекты B, E, G, H, I и J определяются как мусор. Поскольку A, C, D, F по-прежнему доступны для повторного использования с помощью кода приложения, изображенного стрелками из желтой рамки выше.

Сборщик мусора сканирует очередь завершения в поисках указателей на эти объекты. Когда указатель найден, он удаляется из очереди завершения и добавляется в свободную очередь («F-достижимая»). Freachable очереди еще одна внутренняя структура данных под контролем сборщика мусора. Каждый указатель в свободной очереди идентифицирует объект, который готов вызвать свой метод.

Finalize

После коллекции (1st Collection) управляемая куча выглядит примерно так, как показано на рисунке ниже. Объяснение приведено ниже:
1.) Память, занимаемая объектами B, G и H, была немедленно восстановлена, поскольку у этих объектов не было метода finalize, который нужно было вызывать .

2.) Однако память, занятая объектами E, I и J, не может быть восстановлена, поскольку их Finalizeметод еще не был вызван. Вызов метода Finalize осуществляется через свободную очередь.

3.) A, C, D, F по-прежнему подлежат повторной проверке кодом приложения, изображенным стрелками из желтой рамки выше, поэтому они НЕ будут собираться в любом случае

введите описание изображения здесь

Существует специальный поток времени выполнения, посвященный вызову методов Finalize. Когда свободная очередь пуста (что обычно имеет место), этот поток спит. Но когда появляются записи, этот поток просыпается, удаляет каждую запись из очереди и вызывает метод Finalize каждого объекта. Сборщик мусора сжимает исправляемую память, а специальный поток времени выполнения очищает свободную очередь, выполняя Finalizeметод каждого объекта . Так вот, наконец, когда ваш метод Finalize выполняется

В следующий раз, когда вызывается сборщик мусора (2nd Collection), он видит, что завершенные объекты действительно являются мусором, так как корни приложения не указывают на него, а очередь с доступным доступом больше не указывает на него (это тоже ПУСТО), поэтому память для объектов (E, I, J) просто восстанавливается из кучи. См. рисунок ниже и сравните его с рисунком чуть выше

введите описание изображения здесь

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

Примечание :: freachable очередь считается корнем так же , как глобальные и статические переменные корни. Следовательно, если объект находится в свободной очереди, то этот объект достижим и не является мусором.

И последнее замечание: помните, что отладка приложения - это одно, а сборка мусора - это другое и работает по-другому. Пока вы не можете ЧУВСТВОВАТЬ сборку мусора только путем отладки приложений, далее, если вы хотите исследовать память, начните здесь.

RC
источник