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


8
GC не одразу звільняє випадки, коли вони поза сферою. Це робить, коли вважає за потрібне. Ви можете прочитати все про GC тут: msdn.microsoft.com/en-US/library/vstudio/0xy59wtx.aspx
user1908061

@ user1908061 (Pssst. Ваше посилання порушено.)
Драгомок

Відповіді:


352

Ви тут стикаєтесь і робите дуже неправильні висновки, оскільки використовуєте відладчик. Вам потрібно буде запустити свій код так, як він працює на машині вашого користувача. Перейдіть до версії версії спочатку за допомогою менеджера збірки + конфігурація, змініть комбінацію "Конфігурація активного рішення" у верхньому лівому куті на "Випуск". Далі перейдіть до Інструменти + Параметри, Налагодження, Загальне та зніміть прапорець "Придушити оптимізацію JIT".

Тепер запустіть програму ще раз і повозиться з вихідним кодом. Зверніть увагу, як додаткові брекети взагалі не мають ефекту. І зверніть увагу, що встановлення змінної на null взагалі не має ніякої різниці. Це завжди буде друкувати "1". Тепер він працює так, як ви сподіваєтесь, і сподівався, що буде працювати.

Що не залишає перед собою завдання пояснити, чому це працює так інакше, коли ви запускаєте збірку налагодження. Це вимагає пояснення того, як збирач сміття виявляє локальні змінні та як на це впливає наявність налагоджувача.

По-перше, тремтіння виконує два важливі завдання, коли збирає ІЛ для методу в машинний код. Перший дуже добре видно на відладчику, машинний код ви можете побачити за допомогою вікна Debug + Windows + Disassembly. Однак другий обов'язок є абсолютно невидимим. Він також створює таблицю, яка описує, як використовуються локальні змінні всередині методу. Ця таблиця містить запис для кожного аргументу методу та локальну змінну з двома адресами. Адреса, де змінна спочатку зберігатиме посилання на об'єкт. І адреса інструкції машинного коду, де ця змінна вже не використовується. Також, чи зберігається ця змінна у фреймі стека чи в регістрі процесора.

Ця таблиця є важливою для збирача сміття, вона повинна знати, де шукати посилання на об'єкти, коли він виконує збір. Це досить легко зробити, коли посилання є частиною об'єкта на купі GC. Безумовно, це не просто зробити, коли посилання на об'єкт зберігається в регістрі процесора. У таблиці вказано, де шукати.

Адреса "більше не використовується" в таблиці дуже важлива. Це робить сміттєзбірник дуже ефективним . Він може збирати посилання на об'єкт, навіть якщо він використовується всередині методу, і цей метод ще не завершив виконання. Що є дуже поширеним, наприклад, ваш Main () метод, коли-небудь припинить виконання лише до завершення програми. Зрозуміло, ви б не хотіли, щоб жодні посилання на об'єкти, використовувані всередині цього методу Main (), існували протягом тривалості програми, що може означати витік. Джиттер може скористатися таблицею, щоб виявити, що така локальна змінна вже не корисна, залежно від того, наскільки програма просунулася всередині цього методу Main () перед тим, як здійснити виклик.

Майже магічний метод, пов’язаний із цією таблицею, - GC.KeepAlive (). Це дуже особливий метод, він взагалі не генерує жодного коду. Єдиний її обов'язок - це змінити цю таблицю. Він поширюєтьсятермін експлуатації локальної змінної, що запобігає зібранню сміття. Єдиний раз, коли вам потрібно це використовувати, - це не допустити перенапруги GC до збору посилання, що може статися в сценаріях інтероп, коли посилання передається на некерований код. Колекціонер сміття не може бачити, як такі посилання використовуються таким кодом, оскільки він не був складений тремтінням, тому немає таблиці, де сказано, де шукати посилання. Передача об'єкта-делегата некерованій функції на зразок EnumWindows () - це приклад використання котла, коли потрібно використовувати GC.KeepAlive ().

Отже, як ви можете сказати зі свого фрагмента зразка після запуску його у збірці випуску, локальні змінні можуть бути зібрані достроково, перш ніж метод завершиться виконанням. Ще сильніше об'єкт може бути зібраний під час запуску одного з його методів, якщо цей метод більше не посилається на це . У цьому є проблема, налагодити такий метод дуже незручно. Оскільки ви цілком можете помістити змінну у вікно Watch або переглянути її. І він би зникав під час налагодження, якщо виникає GC. Це було б дуже неприємно, тому тремтіння знає, що там додається налагоджувач. Потім він модифікуєтьсятаблиця та змінює "останньо використану" адресу. І змінює його зі свого звичайного значення на адресу останньої інструкції методу. Що підтримує змінну живою, поки метод не повернувся. Що дозволяє продовжувати спостерігати, поки метод не повернеться.

Це також пояснює те, що ви бачили раніше, і чому ви задали це питання. Він друкує "0", оскільки виклик GC.Collect не може зібрати посилання. У таблиці говорить , що змінне використовуються останнім виклику GC.Collect (), аж до кінця цього методу. Змусив це сказати, додавши налагоджувач і запустивши збірку налагодження.

Встановлення змінної в null зараз має ефект, оскільки GC буде перевіряти змінну і більше не побачить посилання. Але переконайтеся, що ви не потрапите в пастку, в яку потрапило багато програмістів на C #, адже писати цей код було безглуздо. Не має жодної різниці, чи існує цей випадок під час запуску коду у версії Release. Насправді оптимізатор тремтіння видалить це твердження, оскільки воно не має жодного ефекту. Тому не забудьте писати такий код, навіть якщо це, здавалося, має ефект.


Останнє зауваження щодо цієї теми - це те, що програмістам стає проблем, які пишуть невеликі програми, щоб зробити щось із додатком 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) увімкнено. Це налаштування за замовчуванням у конфігурації "Випуск". Але якщо використовується спеціальна конфігурація, важливо знати, що ця настройка важлива.
Нуль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 виявляє, що ці об'єкти мають Finalizeметоди і покажчики на ці об'єкти додаються до черги завершення .

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

Коли відбувається GC (1-я колекція), об'єкти B, E, G, H, I і J визначаються як сміття. Оскільки A, C, D, F все ще доступні за допомогою коду програми, зображеного стрілками з жовтого поля вгорі.

Збирач сміття сканує чергу для завершення, шукаючи вказівники на ці об’єкти. Коли вказівник знайдений, вказівник видаляється з черги завершення та додається до чергової черги ("F-reachable"). Freachable черзі ще одна внутрішня структура даних під контролем збирача сміття. Кожен покажчик у черговій доступній черзі ідентифікує об'єкт, готовий викликати його метод.

Finalize

Після колекції (1-я колекція) керована купа виглядає щось подібне до малюнка нижче. Пояснення, наведені нижче:
1.) Пам'ять, яку займають об'єкти B, G і H, була повернена негайно, оскільки ці об'єкти не мали методу доопрацювання, який потрібно було викликати .

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

3.) A, C, D, F все ще доступні за допомогою коду програми, зображеного стрілками з жовтого поля вгорі, тому вони НЕ будуть зібрані ні в якому разі

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

Існує спеціальний потік виконання, присвячений виклику методів Finalize. Коли пустка черги порожня (що зазвичай буває), ця нитка спить. Але коли з'являються записи, цей потік прокидається, видаляє кожен запис із черги та викликає метод Finalize кожного об'єкта. Колектор для сміття ущільнює пам'ять, що відновлюється , а спеціальна нитка виконання спорожняє чергову чергу, виконуючи Finalizeметод кожного об'єкта . Отже, ось нарешті, коли ваш метод Finalize буде виконаний

Наступного разу, коли викликається збирач сміття (друга колекція), він бачить, що завершені об'єкти є справді сміттям, оскільки коріння програми на нього не вказують, і чергова черга вже не вказує на нього (це теж ВИПУСК ), тому пам'ять для об'єктів (E, I, J) просто відтворена з Heap. Перегляньте малюнок нижче і порівняйте його з фігурою трохи вище

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

Тут важливо розуміти, що для відновлення пам’яті, що використовується об’єктами, які потребують доопрацювання, потрібно два GC . Насправді потрібно навіть більше двох кабін колекцій, оскільки ці предмети можуть бути рекламовані для старшого покоління

Примітка :: freachable чергу вважається коренем так само , як глобальні і статичні змінні коріння. Тому, якщо об’єкт стоїть на черговій доступній черзі, то об’єкт доступний і не є сміттям.

В якості останньої замітки пам’ятайте, що програма налагодження - це одне, Garbage Collection - інша справа і працює по-іншому. Поки ви не можете ПІДТРИМАТИ збір сміття лише шляхом налагодження програм, далі, якщо ви хочете дослідити пам'ять, почніть тут.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.