.NET垃圾收集器是否执行代码的预测分析?

Tho*_*que 31 .net c# jit garbage-collection

好吧,我意识到这个问题可能看起来很奇怪,但我只是注意到了一些令我困惑的事情......看看这段代码:

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);
}
Run Code Online (Sandbox Code Playgroud)

由于o1o2仍然在范围时,垃圾回收时,我本来期望的输出如下:

o1还活着:真正的
o2还活着:真的

但相反,这就是我得到的:

o1还活着:假
o2还活着:假

注意:仅当代码在发布模式下编译并在调试器外部运行时才会发生这种情况

我的猜测是GC检测到它o1并且o2在它们超出范围之前不会再次使用,并尽早收集它们.为了验证这个假设,我在TestGC方法的最后添加了以下行:

string s = o2.ToString();
Run Code Online (Sandbox Code Playgroud)

我得到以下输出:

o1还活着:假
o2还活着:是的

所以在这种情况下,o2不收集.

有人能说清楚发生了什么吗?这与JIT优化有关吗?到底发生了什么?

ang*_*son 23

垃圾收集器依赖于编译到 JIT编译器提供的程序集中的信息,该信息告诉它什么代码地址范围各种变量和"事物"仍在使用中.

因此,在您的代码中,由于您不再使用对象变量,因此GC可以自由地收集它们.WeakReference不会阻止这一点,事实上,这是WR的全部要点,允许您保持对对象的引用,同时不阻止它被收集.

有关WeakReference对象的案例很好地总结在MSDN上的单行描述中:

表示弱引用,它引用一个对象,同时仍然允许通过垃圾回收来回收该对象.

WeakReference对象不是垃圾收集的,因此您可以安全地使用它们,但它们引用的对象只剩下WR引用,因此可以自由收集.

当通过调试器执行代码时,变量被人为地扩展到范围,直到它们的范围结束,通常是它们被声明的块的结尾(如方法),以便您可以在断点处检查它们.

这有一些微妙的事情要发现.请考虑以下代码:

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在Release模式下,在没有附加调试器的情况下执行,这是输出:

The value of Value: 15
Test collected
Leaving Test.Execute

这样做的原因是,即使您仍在执行与Test对象关联的方法内部,在要求GC执行此操作时,也不需要对Test进行任何实例引用(不引用thisValue),并且不会调用任何实例方法来执行,因此可以安全地收集对象.

如果您不了解它,可能会产生一些令人讨厌的副作用.

考虑以下课程:

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
    }
Run Code Online (Sandbox Code Playgroud)

在这一点上,如果Test在获取非托管句柄的副本后没有使用任何实例数据怎么办?如果GC现在在我写"危险"的地方运行怎么办?你知道这是怎么回事吗?当GC运行时,它将执行终结器,它将从正在执行的Test下取出对非托管资源的访问.

非托管资源(通常通过IntPtr或类似访问)对垃圾收集器是不透明的,在判断对象的生命周期时不会考虑这些资源.

换句话说,我们在局部变量中保留对句柄的引用对于GC是没有意义的,它只注意到没有实例引用,因此认为对象是安全的.

这个if程序假设没有外部引用仍然被认为是"活着"的对象.例如,如果从以下方法使用上述类:

public void DoSomething()
{
    ClassThatHoldsUnmanagedResource = new ClassThatHoldsUnmanagedResource();
    ClassThatHoldsUnmanagedResource.Test();
}
Run Code Online (Sandbox Code Playgroud)

然后你有完全相同的问题.

(当然,您可能不应该像这样使用它,因为它实现了IDisposable,您应该使用using-block或Dispose手动调用.)

编写上述方法的正确方法是强制GC在我们仍然需要时不会收集我们的对象:

public void Test()
{
    IntPtr local = _HandleToSomethingUnmanaged;

    ... access resource through local here

    GC.KeepAlive(this); // won't be collected before this has executed
}
Run Code Online (Sandbox Code Playgroud)


Han*_*ant 12

垃圾收集器从JIT编译器获取生命周期提示.因此,它知道在GC.Collect()调用时,不再有可能引用局部变量,因此可以收集它们.查看GC.KeepAlive()

附加调试器时,将禁用JIT优化,并将生命周期提示扩展到方法的末尾.使调试变得更加简单.

我写了一个更详细的答案,你会在这里找到.

  • 他对JIT提供这些信息是正确的.我会编辑我的答案. (3认同)