Сбор мусора должен был удалить объект, но 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, чтобы доказать, что объект не был сохранен, было намного сложнее, чем я надеялся. (У нас были ошибки в прошлом с нашим кешем, который сохранил его.)