Сбор мусора должен был удалить объект, но WeakReference.IsAlive все еще возвращает true

У меня есть тест, который я ожидал пройти, но поведение сборщика мусора не так, как я предполагал:

[Test]
public void WeakReferenceTest2()
{
    var obj = new object();
    var wRef = new WeakReference(obj);

    wRef.IsAlive.Should().BeTrue(); //passes

    GC.Collect();

    wRef.IsAlive.Should().BeTrue(); //passes

    obj = null;

    GC.Collect();

    wRef.IsAlive.Should().BeFalse(); //fails
}

В этом примере объект obj должен быть GC'd, поэтому я ожидаю, что свойство WeakReference.IsAlive вернет false.

Похоже, что поскольку переменная obj была объявлена ​​в той же области, что и GC.Collect, она не собирается. Если я перемещаю объявление obj и инициализацию вне метода, тест проходит.

Есть ли у кого-нибудь техническая справочная документация или объяснение этого поведения?

Ответ 1

Ударьте ту же проблему, что и вы - мой тест проходил всюду, за исключением под NCrunch (может быть любой другой инструмент в вашем случае). Гектометр Отладка с помощью SOS выявила дополнительные корни, хранящиеся в стеке вызовов тестового метода. Я предполагаю, что они были результатом использования кода, который отключил оптимизацию компилятора, включая те, которые правильно вычисляют достижимость объекта.

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

[TestMethod]
public void WeakReferenceTest2()
{
    var wRef2 = CallInItsOwnScope(() =>
    {
        var obj = new object();
        var wRef = new WeakReference(obj);

        wRef.IsAlive.Should().BeTrue(); //passes

        GC.Collect();

        wRef.IsAlive.Should().BeTrue(); //passes
        return wRef;
    });

    GC.Collect();

    wRef2.IsAlive.Should().BeFalse(); //used to fail, now passes
}

private T CallInItsOwnScope<T>(Func<T> getter)
{
    return getter();
}

Ответ 2

Есть несколько потенциальных проблем, которые я вижу:

  • Я ничего не знаю о спецификации С#, которая требует ограниченного времени жизни локальных переменных. В не-отладочной сборке я думаю, что компилятор мог бы опустить последнее присвоение obj (установив его на null), поскольку никакой путь кода не приведет к тому, что значение obj никогда не будет использоваться после него, но я ожидал бы, что в сборке без отладки метаданные будут указывать, что переменная никогда не используется после создания слабой ссылки. В сборке отладки переменная должна существовать во всей области функции, но оператор obj = null; должен действительно очистить ее. Тем не менее, я не уверен, что С# spec promises, что компилятор не пропустит последний оператор и все же сохранит переменную.

  • Если вы используете параллельный сборщик мусора, возможно, что GC.Collect() запускает немедленный запуск коллекции, но сбор фактически не будет завершен до того, как GC.Collect() вернется. В этом случае нет необходимости ждать завершения всех финализаторов, и, таким образом, GC.WaitForPendingFinalizers() может быть переполненным, но это, вероятно, решит проблему.

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

Обратите внимание, что обычно не следует ожидать, что слабые ссылки будут признаны недействительными с какой-либо определенной степенью своевременности, и не следует ожидать, что выборка Target после IsAlive отчетов true приведет к ненулевой ссылке. Использовать IsAlive следует только в тех случаях, когда его не интересует цель, если она еще жива, но будет интересно узнать, что ссылка умерла. Например, если у вас есть коллекция объектов WeakReference, можно периодически перебирать список и удалять объекты WeakReference, цель которых умерла. Нужно быть готовым к тому, что WeakReferences может оставаться в коллекции дольше, чем это было бы идеально необходимо; единственным следствием, если они это сделают, должна быть небольшая потеря памяти и процессорного времени.

Ответ 3

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

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

GC.Collect(2, GCCollectionMode.Forced, true);

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

Ответ 4

Может ли быть, что метод расширения .Should() как-то висит на ссылке? Или, возможно, некоторые другие аспекты тестовой среды вызывают эту проблему.

(я отправляю это как ответ, иначе я не могу легко опубликовать код!)

Я пробовал следующий код и работает так, как ожидалось (Visual Studio 2012,.Net 4, debug и release, 32-разрядная и 64-разрядная версии, работающая на Windows 7, четырехъядерный процессор):

using System;

namespace Demo
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var obj = new object();
            var wRef = new WeakReference(obj);

            GC.Collect();
            obj = null;
            GC.Collect();

            Console.WriteLine(wRef.IsAlive); // Prints false.
            Console.ReadKey();
        }
    }
}

Что произойдет, если вы попробуете этот код?

Ответ 5

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

У меня были проблемы с много лет назад при написании unit test и напомнить, что WaitForPendingFinalizers() помогли, так же как и при вызове GC.Collect().

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