Сборка мусора должна была удалить объект, но 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 и инициализацию вне метода, тест проходит.

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

5 ответов


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

лечение здесь довольно простое -никогда не держите сильные ссылки от a метод, который делает 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();
}

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

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

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

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

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


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

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

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

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


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

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

Я пробовал следующий код, и он работает так, как ожидалось (Visual Studio 2012, .Net 4 build, debug and 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();
        }
    }
}

что происходит, когда вы попробовать этот код?


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

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