好的,我意识到这个问题可能看起来很奇怪,但我只是注意到一些令我困惑的事情......看看这段代码:
static void TestGC()
{
object o1 = new Object();
object o2 = new Object();
WeakReference w1 = new WeakReference(o1);
WeakReference w2 = new WeakReference(o2);
GC.Collect();
Console.WriteLine("o1 is alive: {0}", w1.IsAlive);
Console.WriteLine("o2 is alive: {0}", w2.IsAlive);
}
由于o1
和o2
在垃圾收集发生时仍在范围内,我希望得到以下输出:
o1还活着:真实 o2还活着:真的
但相反,这就是我得到的:
o1还活着:假 o2还活着:错误
注意:仅当代码在发布模式下编译并在调试器外部运行时才会出现
我的猜测是,GC检测到o1
和o2
在超出范围之前不会再次使用,并提前收集它们。为了验证这个假设,我在TestGC
方法的末尾添加了以下行:
string s = o2.ToString();
我得到了以下输出:
o1还活着:假 o2还活着:真的
因此,在这种情况下,不会收集o2
。
有人可以了解正在发生的事情吗?这与JIT优化有关吗?究竟发生了什么?
答案 0 :(得分:23)
垃圾收集器依赖于JIT编译器提供的编译到程序集中的信息,它告诉它什么代码地址范围各种变量和“东西”仍在使用中。
因此,在您的代码中,由于您不再使用对象变量,GC可以自由地收集它们。 WeakReference不会阻止这一点,事实上,这是WR的全部要点,允许你保持对一个对象的引用,同时不阻止它被收集。
有关WeakReference个对象的案例很好地总结在MSDN的单行描述中:
表示一个弱引用,它引用一个对象,同时仍然允许通过垃圾回收来回收该对象。
WeakReference对象不是垃圾收集的,因此您可以安全地使用它们,但它们引用的对象只剩下WR引用,因此可以自由收集。
当通过调试器执行代码时,变量被人为地扩展到范围,直到它们的范围结束,通常是它们被声明的块的结尾(如方法),以便您可以在断点处检查它们。 / p>
有一些微妙的事情要发现。请考虑以下代码:
using System;
namespace ConsoleApplication20
{
public class Test
{
public int Value;
~Test()
{
Console.Out.WriteLine("Test collected");
}
public void Execute()
{
Console.Out.WriteLine("The value of Value: " + Value);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Console.Out.WriteLine("Leaving Test.Execute");
}
}
class Program
{
static void Main(string[] args)
{
Test t = new Test { Value = 15 };
t.Execute();
}
}
}
在Release模式下,在没有附加调试器的情况下执行,这是输出:
The value of Value: 15 Test collected Leaving Test.Execute
这样做的原因是,即使您仍在执行与Test对象关联的方法内部,在要求GC执行此操作时,也不需要对Test进行任何实例引用(不参考this
或Value
),并且不会调用任何实例方法,因此可以安全地收集对象。
如果您不了解它,可能会产生一些令人讨厌的副作用。
考虑以下课程:
public class ClassThatHoldsUnmanagedResource : IDisposable
{
private IntPtr _HandleToSomethingUnmanaged;
public ClassThatHoldsUnmanagedResource()
{
_HandleToSomethingUnmanaged = (... open file, whatever);
}
~ClassThatHoldsUnmanagedResource()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
(release unmanaged resource here);
... rest of dispose
}
public void Test()
{
IntPtr local = _HandleToSomethingUnmanaged;
// DANGER!
... access resource through local here
}
此时,如果Test在获取非托管句柄的副本后没有使用任何实例数据,该怎么办?如果GC现在在我写“危险”的地方运行怎么办?你知道这是怎么回事吗?当GC运行时,它将执行终结器,它将从正在执行的Test下取出对非托管资源的访问权。
通常通过IntPtr
或类似方式访问的非托管资源对垃圾收集器不透明,在判断对象的生命周期时不会考虑这些资源。
换句话说,我们在局部变量中保留对句柄的引用对于GC是没有意义的,它只会注意到没有实例引用,因此认为该对象是安全的。
这个if课程假设没有外部引用仍然被认为是“活着”的对象。例如,如果上述类是从这样的方法中使用的:
public void DoSomething()
{
ClassThatHoldsUnmanagedResource = new ClassThatHoldsUnmanagedResource();
ClassThatHoldsUnmanagedResource.Test();
}
然后你有完全相同的问题。
(当然,您可能不应该像这样使用它,因为它实现了IDisposable,您应该使用using
- 阻止或手动调用Dispose
。)
编写上述方法的正确方法是强制GC在我们仍然需要时不会收集我们的对象:
public void Test()
{
IntPtr local = _HandleToSomethingUnmanaged;
... access resource through local here
GC.KeepAlive(this); // won't be collected before this has executed
}
答案 1 :(得分:12)
垃圾收集器从JIT编译器获取生命周期提示。因此,它知道在GC.Collect()调用时,不再有可能引用局部变量,因此可以收集它们。查看GC.KeepAlive()
附加调试器时,将禁用JIT优化,并将生命周期提示扩展到方法的末尾。使调试变得更加简单。
我写了一个更详细的答案,你会发现it here。
答案 2 :(得分:0)
我不是C#专家但我会说这是因为在制作中你的代码是优化的
static void TestGC()
{
WeakReference w1 = new WeakReference(new Object());
WeakReference w2 = new WeakReference(new Object());
GC.Collect();
Console.WriteLine("o1 is alive: {0}", w1.IsAlive);
Console.WriteLine("o2 is alive: {0}", w2.IsAlive);
}
不再有o1,o2结合仍然存在。
编辑:这是一个名为常量折叠的编译器优化。 无论是否有JIT都可以做到这一点。
如果您有办法禁用JIT但保留了发布优化,那么您将会遇到相同的行为。
人们应该注意这个说明:
注意:这仅在代码时发生 在发布模式下编译并运行 在调试器外面
这是关键。
PS:我也假设您了解WeakReference的含义。
答案 3 :(得分:0)
有人可以了解正在发生的事情吗?
虚拟机垃圾收集资源的心理模型非常简单。特别是,您假设变量和范围与垃圾收集有某种关系是错误的。
这与JIT优化有关吗?
是
到底发生了什么?
JIT没有打扰不必要的引用,所以跟踪收集器在踢它时没有找到引用,因此它收集了对象。
请注意,其他答案表明,当事实上JIT 没有将这些引用传达给GC时,JIT将此信息传达给GC,因为这样做是没有意义的