Понимание коллекции мусора в .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 в основном методе выходит за пределы области действия и больше не ссылается на какой-либо другой объект при вызове GC.Collect(), почему он не завершен там?

Ответы

Ответ 1

Вы спотыкаетесь здесь и делаете очень неправильные выводы, потому что используете отладчик. Вам нужно будет запустить свой код так, как он работает на вашей пользовательской машине. Перейдите в сборку Release сначала с помощью менеджера сборки + Configuration, измените комбинацию "Active solution configuration" в левом верхнем углу, чтобы "Отпустить". Затем перейдите в Инструменты + Параметры, Отладка, Общие и отключите опцию "Подавить оптимизацию JIT".

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

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

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

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

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

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

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

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

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


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

Ответ 2

[Просто хотел добавить еще дальше процесс Internals of Finalization]

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

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

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

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

  • Любой объект считается мусором, если он не доступен для приложения    Код

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

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

поэтому указатели на объекты C, E, F, I, J добавляются в очередь финализации.

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

enter image description here

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

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

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

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

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

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

enter image description here

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

В следующий раз, когда вызывается сборщик мусора (2-я сборка), он видит, что финализированные объекты действительно мусор, поскольку корни приложения не указывают на него, а лишняя очередь больше не указывает на него (это тоже ПОРОЖНО), Поэтому память для объектов (E, I, J) просто восстанавливается из диаграммы Heap.See ниже и сравнивает ее с фигурой чуть выше

enter image description here

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

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

Как последнее примечание, помните, что приложение для отладки - это одно, а Garbage Collection - это другое дело и работает по-другому. Пока вы не можете обманывать сбор мусора, просто отлаживая приложения, а затем, если вы хотите исследовать память, получите здесь.

Ответ 3

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

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

1) Уберите

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

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

Используйте GC.SuppressFinalize(это), чтобы предотвратить вызов Finalizer, если вы уже использовали dispose()

2) Finalize или Distructor

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

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

3) GC.Collect()

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

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

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

Обратитесь к сообщению в моем блоге, где у меня есть эта статья: Коллекция мусора в .NET