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

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

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() называется, почему это не завершено там?

4 ответа

Решение

Вы попадаете сюда и делаете очень неправильные выводы, потому что используете отладчик. Вам нужно будет запустить свой код так, как он работает на компьютере вашего пользователя. Сначала перейдите к сборке выпуска с помощью диспетчера сборки + конфигурации, измените комбо "Конфигурация активного решения" в верхнем левом углу на "Выпуск". Затем перейдите в Инструменты + Параметры, Отладка, Общие и снимите флажок "Подавить оптимизацию 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() на самом деле работает, только не при отладке приложения.

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

Итак, вы создаете объект, и когда объект собирается, объект 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-достижимая").

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

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

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

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

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

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

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

ПРИМЕЧАНИЕ:: Свободная очередь считается корнем, как глобальные и статические переменные являются корнями. Следовательно, если объект находится в свободной очереди, то этот объект достижим и не является мусором.

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

Существует 3 способа управления памятью:

GC работает только для управляемых ресурсов, поэтому.NET предоставляет Dispose и Finalize для освобождения неуправляемых ресурсов, таких как поток, соединение с базой данных, объекты COM и т. Д.

1) Утилизировать

Dispose должен вызываться явно для типов, которые реализуют IDisposable.

Программист должен вызывать это либо с помощью Dispose(), либо с помощью использования конструкции

Используйте GC.SuppressFinalize(this) для предотвращения вызова Finalizer, если вы уже использовали dispose ()

2) Завершение или Дистрибьютор

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

Недостаток реализации финализатора заключается в том, что восстановление памяти задерживается, так как финализатор для таких классов / типов должен называться предварительной очисткой, поэтому дополнительный сбор для восстановления памяти.

3) GC.Collect()

Использование GC.Collect() не обязательно помещает GC в коллекцию, GC может переопределять и запускать, когда захочет.

также GC.Collect() будет запускать только трассирующую часть сборки мусора и добавлять элементы в очередь финализаторов, но не вызывать финализаторы для типов, которые обрабатываются другим потоком.

Используйте WaitForPendingFinalizers, если вы хотите убедиться, что все финализаторы были вызваны после того, как вы вызвали GC.Collect()

Рассмотрите следующий код и определите, какое из объяснений лучше всего представляет состояние ссылок c1, c2 и c3, прямо перед окончанием метода main(), при условии, что сборка мусора не выполнялась? На диаграммах каждый квадрат представляет собой объект «Курица» с определенным количеством яиц.

Другие вопросы по тегам