Подтвердить что ты не робот

Собирает ли сборщик мусора .NET прогностический анализ кода?

Хорошо, я понимаю, что этот вопрос может показаться странным, но я только что заметил то, что действительно озадачило меня... Посмотрите на этот код:

static void TestGC()
{
        object o1 = new Object();
        object o2 = new Object();
        WeakReference w1 = new WeakReference(o1);
        WeakReference w2 = new WeakReference(o2);

        GC.Collect();

        Console.WriteLine("o1 is alive: {0}", w1.IsAlive);
        Console.WriteLine("o2 is alive: {0}", w2.IsAlive);
}

Так как o1 и o2 все еще находятся в области видимости, когда происходит сбор мусора, я ожидал следующий вывод:

o1 жив: True
o2 жив: True

Но вместо этого вот что я получил:

o1 жив: False
o2 жив: False

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

Мое предположение заключается в том, что GC обнаруживает, что o1 и o2 больше не будут использоваться до того, как они выйдут из области действия, и собирают их раньше. Чтобы проверить эту гипотезу, я добавил следующую строку в конце метода TestGC:

string s = o2.ToString();

И я получил следующий вывод:

o1 жив: False
o2 жив: True

Итак, в этом случае o2 не собирается.

Может ли кто-то пролить свет на то, что происходит? Связано ли это с оптимизацией JIT? Что происходит точно?

4b9b3361

Ответ 1

Сборщик мусора опирается на информацию , скомпилированную в вашу сборку, предоставленную компилятором JIT, которая сообщает ему, что код кода задает различные переменные, а "вещи" все еще используются.

Таким образом, в вашем коде, поскольку вы больше не используете переменные объекта GC, их можно бесплатно собрать. WeakReference не помешает этому, на самом деле, это весь смысл WR, чтобы вы могли ссылаться на объект, не мешая ему собираться.

Случай с WeakReference прекрасно скомпонован в однострочном описании в MSDN:

Представляет собой слабую ссылку, которая ссылается на объект, все еще позволяя этому объекту быть восстановленным путем сбора мусора.

Объекты WeakReference не собираются в мусор, поэтому вы можете безопасно их использовать, но объекты, на которые они ссылаются, оставили только ссылку WR, и, следовательно, их можно было бесплатно собрать.

При выполнении кода через отладчик переменные искусственно расширяются в области действия до тех пор, пока их область действия не закончится, как правило, конец блока, в котором они объявлены (например, методы), чтобы вы могли проверить их в точке останова.

Там есть некоторые тонкие вещи, чтобы обнаружить это. Рассмотрим следующий код:

using System;

namespace ConsoleApplication20
{
    public class Test
    {
        public int Value;

        ~Test()
        {
            Console.Out.WriteLine("Test collected");
        }

        public void Execute()
        {
            Console.Out.WriteLine("The value of Value: " + Value);

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            Console.Out.WriteLine("Leaving Test.Execute");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Test t = new Test { Value = 15 };
            t.Execute();
        }
    }
}

В режиме Release, выполненном без прикрепленного отладчика, здесь вывод:

The value of Value: 15
Test collected
Leaving Test.Execute

Причиной этого является то, что, хотя вы все еще выполняете внутри метода, связанного с объектом Test, в момент запроса GC сделать это, нет необходимости в каких-либо экземплярах ссылок на Test (ссылка на this или Value) и никаких вызовов какого-либо метода-экземпляра, оставшегося для выполнения, поэтому объект можно безопасно собирать.

Это может иметь некоторые неприятные побочные эффекты, если вы не знаете об этом.

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

public class ClassThatHoldsUnmanagedResource : IDisposable
{
    private IntPtr _HandleToSomethingUnmanaged;

    public ClassThatHoldsUnmanagedResource()
    {
        _HandleToSomethingUnmanaged = (... open file, whatever);
    }

    ~ClassThatHoldsUnmanagedResource()
    {
        Dispose(false);
    }

    public void Dispose()
    {
        Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
        (release unmanaged resource here);
        ... rest of dispose
    }

    public void Test()
    {
        IntPtr local = _HandleToSomethingUnmanaged;

        // DANGER!

        ... access resource through local here
    }

На этом этапе, если Test не использует данные экземпляра после захвата копии неуправляемого дескриптора? Что делать, если GC теперь работает в точке, где я написал "ОПАСНОСТЬ"? Вы видите, где это происходит? Когда GC запускается, он выполнит финализатор, который выдержит доступ к неуправляемому ресурсу из теста Test, который все еще выполняется.

Неуправляемые ресурсы, обычно доступные через IntPtr или подобные, непрозрачны сборщику мусора и не учитывают их при оценке срока жизни объекта.

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

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

public void DoSomething()
{
    ClassThatHoldsUnmanagedResource = new ClassThatHoldsUnmanagedResource();
    ClassThatHoldsUnmanagedResource.Test();
}

Тогда у вас есть та же самая проблема.

(конечно, вы, вероятно, не должны использовать его так, поскольку он реализует IDisposable, вы должны использовать using -block или call Dispose вручную.)

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

public void Test()
{
    IntPtr local = _HandleToSomethingUnmanaged;

    ... access resource through local here

    GC.KeepAlive(this); // won't be collected before this has executed
}

Ответ 2

Сборщик мусора получает подсказки жизни от JIT-компилятора. Таким образом, он знает, что при вызове GC.Collect() нет более возможных ссылок на локальные переменные, и поэтому они могут быть собраны. Обзор GC.KeepAlive()

Когда подключен отладчик, оптимизация JIT отключена, и подсказка на всю жизнь расширяется до конца метода. Делает отладку намного проще.

Я написал гораздо более подробный ответ об этом, вы найдете здесь.

Ответ 3

Я не эксперт на С#, но я бы сказал, что это потому, что в вашем продукте ваш код оптимизирован в

static void TestGC()
{
        WeakReference w1 = new WeakReference(new Object());
        WeakReference w2 = new WeakReference(new Object());

        GC.Collect();

        Console.WriteLine("o1 is alive: {0}", w1.IsAlive);
        Console.WriteLine("o2 is alive: {0}", w2.IsAlive);
}

Больше не осталось o1, o2.

EDIT: Это оптимизация компилятора с постоянной сгибанием. Что можно сделать с JIT или без него.

Если у вас есть способ отключить JIT, но сохранил оптимизацию выпуска, у вас будет такое же поведение.

Люди должны обратить внимание на примечание:

ПРИМЕЧАНИЕ: это происходит только тогда, когда код скомпилирован в режиме Release и запускается вне отладчика

это ключ.

PS: Я также предполагаю, что вы понимаете, что означает WeakReference.

Ответ 4

Может кто-то пролил свет на то, что происходит?

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

Связано ли это с оптимизацией JIT?

Да.

Что происходит точно?

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

Обратите внимание, что в других ответах указано, что JIT передал эту информацию в GC, когда правда заключается в том, что JIT не передавал эти ссылки на GC, потому что не было бы никакого смысла в этом.