代码之家  ›  专栏  ›  技术社区  ›  Thomas Levesque

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

  •  29
  • Thomas Levesque  · 技术社区  · 15 年前

    好吧,我意识到这个问题可能看起来很奇怪,但我只是注意到一些真正让我困惑的事情…请看一下此代码:

    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还活着:是的
    氧气还活着:是的

    但是,我得到的是:

    O1还活着:假
    氧气还活着:假

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

    我猜是GC检测到的 O1 O2 在它们超出范围并提前收集之前不会再次使用。为了验证这个假设,我在 TestGC 方法:

    string s = o2.ToString();
    

    我得到了以下输出:

    O1还活着:假
    氧气还活着:是的

    所以在这种情况下, O2 未收集。

    有人能解释一下发生了什么事吗?这与JIT优化有关吗?到底发生了什么?

    4 回复  |  直到 12 年前
        1
  •  21
  •   Lasse V. Karlsen    15 年前

    垃圾收集器依赖于信息 编译到程序集中 由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();
            }
        }
    }
    

    在发布模式下,在未附加调试程序的情况下执行,输出如下:

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

    这样做的原因是,即使您仍然在与测试对象相关联的方法中执行,在请求GC执行它时,也不需要任何实例引用来测试(不引用 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
        }
    

    此时,如果测试在获取非托管句柄的副本后不使用任何实例数据,该怎么办?如果GC现在在我写“危险”的地方运行呢?你看到这是要去哪里吗?当GC运行时,它将执行终结器,该终结器将从仍在执行的测试中提取对非托管资源的访问。

    非托管资源,通常通过 IntPtr 或者类似的,对于垃圾收集器来说是不透明的,并且在判断对象的生命时不会考虑这些问题。

    换句话说,我们在局部变量中保留对句柄的引用对GC来说是毫无意义的,它只注意到没有剩余的实例引用,因此认为可以安全地收集对象。

    如果course假定不存在对仍被视为“活动”的对象的外部引用,则会出现这种情况。例如,如果上面的类是从这样的方法中使用的:

    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
    }
    
        2
  •  12
  •   Community Mohan Dere    8 年前

    垃圾收集器从JIT编译器获取生存期提示。因此,它知道在gc.collect()调用中,没有更多可能的对局部变量的引用,因此可以收集它们。查看gc.keepalive()。

    当附加了调试器时,将禁用JIT优化,并将生存期提示扩展到方法的末尾。使调试更简单。

    我写了一个更详细的答案,你会发现 it here .

        3
  •  0
  •   mathk    15 年前

    我不是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,但保持发布优化,那么您将拥有相同的行为。

    人们应该注意以下几点:

    注意:只有当代码 在发布模式下编译并运行 在调试器之外

    这是钥匙。

    附言:我也假设你理解weakreference的意思。

        4
  •  0
  •   J D    13 年前

    有人能解释一下发生了什么事吗?

    关于虚拟机垃圾如何收集资源的心智模型是不现实的简单。尤其是,假设变量和范围与垃圾收集有某种关联是错误的。

    这与JIT优化有关吗?

    对。

    到底发生了什么?

    JIT不需要不必要地保留引用,所以跟踪收集器在踢它时找不到引用,所以它收集了对象。

    注意,其他答案已经表明,当事实是JIT实际上是 没有 将这些引用传递给GC,因为这样做没有意义。