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

Может ли экземпляр класса, который не назначается переменной, получает сбор мусора слишком рано?

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

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

public class MyClass
{
    private int _myVar;

    public void DoSomething()
    {
        // ...Do something...

        _myVar = 1;

        System.Console.WriteLine("Inside");
    }
}

И используя этот класс следующим образом:

public class Test
{
    public static void Main()
    {
        // ...Some code...
        System.Console.WriteLine("Before");

        // No assignment to a variable.
        new MyClass().DoSomething();

        // ...Some other code...
        System.Console.WriteLine("After");
    }
}

(Ideone)

Выше, я создаю экземпляр класса, не присваивая его переменной.

Я боюсь, что сборщик мусора может удалить мой экземпляр слишком рано.

Мое наивное понимание сборки мусора:

"Удалите объект, как только никакие ссылки не указывают на него".

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

Может кто-нибудь дать мне информацию, которую мне не хватает?

Подводя итог, мой вопрос:

(Почему/почему нет) безопасно ли создавать экземпляр класса, не присваивая его переменной или return ing?

т.е. это

new MyClass().DoSomething();

и

var c = new MyClass();
c.DoSomething();

то же самое из точки сбора мусора?

4b9b3361

Ответ 1

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

Объект имеет право на сбор мусора (это не то же самое, что сказать, что он будет немедленно собран мусором), когда GC может доказать, что ничего больше не будет использовать какие-либо его данные.

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

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

Вот демонстрация этой "ранней коллекции" - ну, ранняя финализация в этом случае, как это легче продемонстрировать, но я думаю, что это достаточно ясно доказывает:

using System;
using System.Threading;

class EarlyFinalizationDemo
{
    int x = Environment.TickCount;

    ~EarlyFinalizationDemo()
    {
        Test.Log("Finalizer called");
    }    

    public void SomeMethod()
    {
        Test.Log("Entered SomeMethod");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Thread.Sleep(1000);
        Test.Log("Collected once");
        Test.Log("Value of x: " + x);
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Thread.Sleep(1000);
        Test.Log("Exiting SomeMethod");
    }

}

class Test
{
    static void Main()
    {
        var demo = new EarlyFinalizationDemo();
        demo.SomeMethod();
        Test.Log("SomeMethod finished");
        Thread.Sleep(1000);
        Test.Log("Main finished");
    }

    public static void Log(string message)
    {
        // Ensure all log entries are spaced out
        lock (typeof(Test))
        {
            Console.WriteLine("{0:HH:mm:ss.FFF}: {1}",
                              DateTime.Now, message);
            Thread.Sleep(50);
        }
    }
}

Вывод:

10:09:24.457: Entered SomeMethod
10:09:25.511: Collected once
10:09:25.562: Value of x: 73479281
10:09:25.616: Finalizer called
10:09:26.666: Exiting SomeMethod
10:09:26.717: SomeMethod finished
10:09:27.769: Main finished

Обратите внимание, как объект завершается после печати значения x (так как нам нужен объект для извлечения x), но до завершения SomeMethod.

Ответ 2

Другие ответы хороши, но я хочу подчеркнуть несколько пунктов здесь.

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

Итак, начнем с:

Мое наивное понимание сборки мусора: "Удалите объект, как только никакие ссылки не указывают на него".

Это понимание неверно неверно. Предположим, что

class C { C c; public C() { this.c = this; } }

Теперь каждый экземпляр C имеет ссылку на него, хранящуюся внутри себя. Если объекты были восстановлены только тогда, когда счетчик ссылок был равен нулю, объекты с циркулярной ссылкой никогда не будут очищены.

Правильное понимание:

Некоторые ссылки - это "известные корни". Когда происходит сбор, прослеживаются известные корни. То есть все известные корни живы, и все, что относится к чему-то живое, тоже живое, транзитивно. Все остальное мертво и приемлемо для мелиорации.

Мертвые объекты, требующие завершения, не собираются. Скорее, они сохраняются в очереди финализации, которая является известным корнем, до запуска их финализаторов, после чего они помечены как не требующие окончательной доработки. Будущая коллекция будет идентифицировать их как мертвых во второй раз, и они будут исправлены.

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

Я создаю экземпляр класса, не присваивая его переменной.

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

Приведем пример:

void M()
{
    var resource = OpenAFile();
    int handle = resource.GetHandle();
    UnmanagedCode.MessWithFile(handle);
}

Предположим, что resource - это экземпляр класса с финализатором, а финализатор закрывает файл. Может ли финализатор работать до MessWithFile? Да! Тот факт, что resource является локальной переменной со временем жизни всего тела M, не имеет значения. Время выполнения может понять, что этот код можно оптимизировать:

void M()
{
    int handle;
    {
        var resource = OpenAFile();
        handle = resource.GetHandle();
    }
    UnmanagedCode.MessWithFile(handle);
}

и теперь resource мертво к моменту времени MessWithFile. Маловероятно, но законно, чтобы финализатор работал между GetHandle и MessWithFile, и теперь мы возимся с закрытым файлом.

Правильное решение здесь - использовать GC.KeepAlive на ресурсе после вызова MessWithFile.

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

Поставьте другой путь: вы спросили,

new MyClass().DoSomething();

и

var c = new MyClass();
c.DoSomething();

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

Более короткий ответ на ваш вопрос: доверять сборщику мусора. Он был тщательно написан, чтобы поступать правильно. Единственный раз, когда вам нужно беспокоиться о том, что GC делает неправильную вещь, - это сценарии, подобные тому, который я изложил, где время финализаторов важно для правильности неуправляемых вызовов кода.

Ответ 3

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

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

Пока DoSomething может по-прежнему использовать значение this, он остается в режиме реального времени. Если DoSomething никогда не использует состояние экземпляра, то действительно экземпляр может быть собран, пока вызов метода все еще работает на нем. Это ненаблюдаемо, поэтому безопасно.

Ответ 4

Пока вы говорите об одной потоковой среде, вы в безопасности. Интересные вещи начинаются только тогда, когда вы начинаете новый поток внутри метода DoSomething, и еще больше удовольствия происходит, если ваш класс имеет финализатор. Главное, чтобы понять здесь, состоит в том, что многие контракты между вами и исполняемым файлом/оптимизатором/и т.д. Действительны только в одном потоке. Это одна из вещей, которая имеет катастрофические результаты, когда вы начинаете программировать несколько потоков на языке, который не является primaririly многопоточным ориентированным (да, С# является одним из этих языков).

В вашем случае вы даже используете экземпляр this, что делает неожиданную коллекцию еще менее вероятной, пока она находится внутри этого метода; в любом случае, контракт заключается в том, что в одном потоке вы не можете наблюдать разницу между оптимизированным и неоптимизированным кодом (кроме использования памяти, скорости и т.д., но это "бесплатный обед" ).