C# 数组中的对象未被垃圾回收
我正在测试一个类,该类使用弱引用来确保对象能够被垃圾收集,我发现即使列表不再被引用,列表中的对象也不会被收集。简单数组也是如此。下面的代码片段显示了一个失败的简单测试C# 数组中的对象未被垃圾回收,c#,.net,garbage-collection,C#,.net,Garbage Collection,我正在测试一个类,该类使用弱引用来确保对象能够被垃圾收集,我发现即使列表不再被引用,列表中的对象也不会被收集。简单数组也是如此。下面的代码片段显示了一个失败的简单测试 class TestDestructor { public static bool DestructorCalled; ~TestDestructor() { DestructorCalled = true; } } [Test] public void TestGarbageC
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);
}
忽略数组的初始化将导致测试通过
为什么数组中的对象没有被垃圾收集?如果我没有弄错的话,那是因为当加载到数组中时,对象基本上是被复制的,并且与最初创建的对象分离。然后,销毁阵列和原始对象时,复制到阵列的对象仍然存在
垃圾收集最终应该完成它的工作,但我知道你们试图强迫它清除资源。我要尝试的是在销毁阵列之前先清除阵列(移除该对象),然后查看它是否清除了所有内容。编辑:好的,我正在这方面取得一些进展。可能涉及三个二进制开关(至少):
- 代码是否优化;i、 e.命令行上的
或/o+
标志。这似乎没有什么区别/o-
- 代码是否在调试器中运行。这似乎没有什么区别
- 生成的调试信息级别,即
、/debug+
或/debug-
或/debug:full
命令行标志。只有/debug:pdbonly
或/debug+
会导致它失败/debug:full
- 如果将
代码与Main
代码分开,可以看出是TestDestructor
代码的编译模式造成了差异Main
- 据我所知,在方法本身中为
生成的IL与为/debug:pdbonly
生成的IL相同,因此它可能是一个清单问题/debug:full
ildasm /out:broken.il Program.exe
ilasm broken.il
ilasm有三种不同的调试设置:/debug
、/debug=OPT
和/debug=IMPL
。使用前两种方法中的任何一种都会失败——使用后一种方法,它会工作。最后一个被描述为启用JIT优化,所以这大概就是这里的区别所在。。。虽然在我看来,它应该仍然能够收集任何方式的对象
这可能是由于
析构函数调用的内存模型造成的。它不是易失性的,因此不能保证来自终结器线程的写入被测试线程“看到”
在这种情况下,当然会调用终结器。在将变量设置为volatile之后,这个独立的等效示例(对我来说运行起来更简单)对我来说肯定是真的。当然,这并不是证据:没有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);
}
}
编辑:我刚刚看到在使用VisualStudio构建时失败了,但是从命令行可以看到。现在查看IL…另一个编辑:如果数组在Main()方法范围中定义,则结果将始终为false,但如果在类测试范围中定义,则结果将为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);
}
}
类TestDestructor
{
公共TestDestructor()
{
testList=新列表();
}
已调用公共静态易失性布尔销毁;
~TestDestructor()
{
析构函数调用=真;
}
公共字符串xy=“test”;
公共列表测试列表;
}
课堂测试
{
私有静态对象[]myArray;
静态void Main()
{
NewMethod();
myArray=null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine(TestDestructor.DestructorCalled);
Console.In.ReadToEnd();
}
私有静态void NewMethod()
{
TestDestructor TestDestructor=新的TestDestructor(){xy=“foo”};
testDestructor.testList.Add(“bar”);
myArray=新对象[]{testDestructor};
Console.WriteLine(myArray.Length);
}
}
就是这么说的:
实现Finalize方法或析构函数可能会对性能产生负面影响,您应该避免不必要地使用它们。使用Finalize方法回收对象使用的内存至少需要两次垃圾回收。[…]未来的垃圾收集将确定最终确定的对象确实是垃圾,因为标记为准备完成的对象列表中的条目不再指向它们。在未来的垃圾收集中,对象的内存实际上是回收的
尝试使用一种处理机制,而不是最终确定,以查看将发生什么情况正如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);
}
}
对我来说,它工作(没有挥发性)并且总是打印真实的。是否有人可以确认在发布模式下没有调用终结器,否则我们可以假设它与调试模式相关。只是猜测,但数组是否“真的”创建了?也许它被优化了?确实,在销毁对象之前,是否可以对其执行某些操作?您还应该尝试在析构函数中放置Console.WriteLine。这与生成有关吗?在垃圾收集上,testDestructor仍然作为数组的一个元素存在(即使数组被取消引用?),因此在这个周期内不会被收集。如果收集两次会发生什么情况?Jon Skeet关于需要
volatile
的说法是正确的,这看起来像是一个调试vs发布的问题。@Jon:在发布模式下简要检查IL,看起来C编译器完全省略了数组分配。请注意,断言是针对sta的