我正在测试一个使用弱引用的类,以确保对象能够被垃圾收集,并且我在List<>中找到了对象。即使列表不再被引用,也从未收集过。简单数组也是如此。以下代码段显示了一个失败的简单测试。
class TestDestructor
{
public static bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
}
[Test]
public void TestGarbageCollection()
{
TestDestructor testDestructor = new TestDestructor();
var array = new object[] { testDestructor };
array = null;
testDestructor = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Assert.IsTrue(TestDestructor.DestructorCalled);
}
退出阵列的初始化会导致测试通过。
为什么数组中的对象没有被垃圾收集?
答案 0 :(得分:4)
/o+
或/o-
标志。这似乎没什么区别。/debug+
,/debug-
或/debug:full
或/debug:pdbonly
命令行标志。只有/debug+
或/debug:full
会导致其失败。此外:
Main
代码与TestDestructor
代码分开,则可以告诉它是Main
代码的编译模式,这会产生差异/debug:pdbonly
生成的IL与方法本身中的/debug:full
相同,因此它可能是一个明显的问题...... ildasm /out:broken.il Program.exe
ilasm broken.il
ilasm有三种不同的调试设置:/DEBUG
,/DEBUG=OPT
和/DEBUG=IMPL
。使用前两个中的任何一个,它都失败了 - 使用最后一个,它可以工作。最后一个被描述为启用JIT优化,所以大概这就是在这里有所作为......虽然在我看来它 仍然能够以任何方式收集对象。
这可能是由DestructorCalled
方面的内存模型引起的。它不是易失性的,因此无法保证终结器线程的写入被测试线程“看到”。
在这种情况下,终结者肯定会 。在使变量变为volatile之后,这个独立的等效示例(对我来说简单易用)肯定会为我打印True。当然,这不是证明:没有volatile
代码不能保证失败;它不能保证工作。 将其作为易变变量后,你能否让你的测试失败?
using System;
class TestDestructor
{
public static volatile bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
}
class Test
{
static void Main()
{
TestDestructor testDestructor = new TestDestructor();
var array = new object[] { testDestructor };
array = null;
testDestructor = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
}
}
编辑:我刚刚看到这个在使用Visual Studio构建时失败,但从命令行开始就没问题。现在看看IL ......
答案 1 :(得分:2)
另一个编辑:如果数组是在Main() - Method-Scope中定义的,则结果将始终为false,但如果在Class-Test-Scope中定义,则结果为true。也许这不是一件坏事。
class TestDestructor
{
public TestDestructor()
{
testList = new List<string>();
}
public static volatile bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
public string xy = "test";
public List<string> testList;
}
class Test
{
private static object[] myArray;
static void Main()
{
NewMethod();
myArray = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
Console.In.ReadToEnd();
}
private static void NewMethod()
{
TestDestructor testDestructor = new TestDestructor() { xy = "foo" };
testDestructor.testList.Add("bar");
myArray = new object[] { testDestructor };
Console.WriteLine(myArray.Length);
}
}
答案 2 :(得分:1)
正如Ani在评论中指出的那样,整个数组在发布模式下进行了优化,因此我们应该将代码更改为:
class TestDestructor
{
public static bool DestructorCalled;
~TestDestructor()
{
DestructorCalled = true;
}
}
class Test
{
static void Main()
{
TestDestructor testDestructor = new TestDestructor();
var array = new object[] { testDestructor };
Console.WriteLine(array[0].ToString());
array = null;
testDestructor = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
}
}
对我来说它有效(没有不稳定)并且始终打印为True。任何人都可以确认在释放模式下没有调用终结器,因为否则我们可以假设它与调试模式有关。
答案 3 :(得分:0)
如果我没弄错的话,那是因为对象基本上是复制的,并且在加载到数组中时与其初始创建分开。然后,当您销毁数组和原始对象时,复制到数组的对象仍然存在。
垃圾收集应该最终完成它的工作,但我得到你试图强制它清除资源。我要尝试的是在销毁之前首先清除数组(删除对象),然后查看是否删除了所有内容。
答案 4 :(得分:0)
这就是documentation所说的:
实现Finalize方法或析构函数会对性能产生负面影响,您应该避免不必要地使用它们。使用Finalize方法回收对象使用的内存至少需要两个垃圾回收。 [...]未来的垃圾收集将确定最终的对象是真正的垃圾,因为标记为准备完成的对象列表中的条目不再指向它们。在未来的垃圾收集中,对象的内存实际上是回收的。
尝试使用处理机制而不是最终确定将会发生什么