5
[TestFixture]
public class Tests
{
    private class Relay
    {
        public Action Do { get; set; }
    }

    [Test]
    public void OptimizerStrangeness()
    {
        var relay = new Relay();
        var indicator = 0;
        relay.Do = () => indicator++;
        var weak = new WeakReference(relay);

        GC.Collect();

        var relayNew = weak.Target as Relay;
        if (relayNew == null) Assert.Fail();
        relayNew.Do();
        Assert.AreEqual(1, indicator);
    }
}

Assert.Fail()尽管事实relay变量仍在范围内,但该代码仅在 Release 模式下失败,因此我们仍然对实例有强引用,因此 WeakReference 一定还没有死。

UPD:澄清一下:我意识到它可以“优化掉”。但是取决于这个优化indicator变量将具有01值,即我们有实际可见的行为变化。

UPD2:来自 C# 语言规范,第 3.9 节

如果除了运行析构函数之外,任何可能的继续执行都无法访问该对象或其任何部分,则该对象被视为不再使用,并且它有资格进行销毁。C# 编译器和垃圾收集器可能会选择分析代码以确定将来可能使用对对象的哪些引用。例如,如果作用域内的局部变量是对对象的唯一现有引用,但在从过程中的当前执行点开始的任何可能的继续执行中从未引用该局部变量,则垃圾收集器可能(但不需要)将对象视为不再使用。

从技术上讲,这个对象可以并且将通过继续执行来访问,因此不能被视为“不再使用”(实际上 C# 规范没有提到弱引用,因为它是 CLR 的方面而不是编译器 - 编译器输出很好)。将尝试搜索有关 CLR/JIT 的内存管理信息。

UPD3:以下是有关 CLR 内存管理的一些信息- “释放内存”部分:

...每个应用程序都有一组根。每个根要么引用托管堆上的一个对象,要么设置为空。应用程序的根包括全局和静态对象指针、线程堆栈上的局部变量和引用对象参数,以及 CPU 寄存器。垃圾收集器可以访问即时 (JIT) 编译器和运行时维护的活动根列表。使用此列表,它检查应用程序的根,并在此过程中创建一个包含从根可到达的所有对象的图。

有问题的变量肯定是局部变量,因此它是可访问的。如此说来,这个提及非常快速/模糊,所以我很高兴看到更具体的信息。

UPD4:来自 .NET Framework 的来源:

    // This method DOES NOT DO ANYTHING in and of itself.  It's used to
    // prevent a finalizable object from losing any outstanding references 
    // a touch too early.  The JIT is very aggressive about keeping an 
    // object's lifetime to as small a window as possible, to the point
    // where a 'this' pointer isn't considered live in an instance method 
    // unless you read a value from the instance.  So for finalizable
    // objects that store a handle or pointer and provide a finalizer that
    // cleans them up, this can cause subtle ----s with the finalizer
    // thread.  This isn't just about handles - it can happen with just 
    // about any finalizable resource.
    // 
    // Users should insert a call to this method near the end of a 
    // method where they must keep an object alive for the duration of that
    // method, up until this method is called.  Here is an example: 
    //
    // "...all you really need is one object with a Finalize method, and a
    // second object with a Close/Dispose/Done method.  Such as the following
    // contrived example: 
    //
    // class Foo { 
    //    Stream stream = ...; 
    //    protected void Finalize() { stream.Close(); }
    //    void Problem() { stream.MethodThatSpansGCs(); } 
    //    static void Main() { new Foo().Problem(); }
    // }
    //
    // 
    // In this code, Foo will be finalized in the middle of
    // stream.MethodThatSpansGCs, thus closing a stream still in use." 
    // 
    // If we insert a call to GC.KeepAlive(this) at the end of Problem(), then
    // Foo doesn't get finalized and the stream says open. 
    [System.Security.SecuritySafeCritical]  // auto-generated
    [ResourceExposure(ResourceScope.None)]
    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] 
    public static extern void KeepAlive(Object obj);

如果您有兴趣,请参阅此处了解更多详细信息。

4

1 回答 1

8

即使变量在范围内,如果在以后的任何代码路径中不再访问它,运行时也可以自由地收集它。这就是为什么当调试一个带有 JIT 优化的程序集时,即使它当前在范围内,它也会给你一条关于变量值被优化掉的消息。

请参阅3.9 自动内存管理中的第 2 项。

再具体一点,

例如,如果作用域内的局部变量是对对象的唯一现有引用,但在从过程中的当前执行点开始的任何可能的继续执行中从未引用该局部变量,则垃圾收集器可能(但不需要)将对象视为不再使用。

这是关键点;该对象被认为是不可访问的,因为对该对象的所有强引用(只有一个)都是不可访问的。请记住,C# 规范将包含有关语言的信息以及有关编译代码应如何执行的信息。还要记住,可达性不是由范围定义的。正如规范所述,如果编译器和运行时可以确定变量不存在于任何未来的代码路径中(这意味着它们根本不会被引用或仅在被确定为无法访问的路径中被引用,例如if(false)),那么变量是被认为是不可访问的,不能算作强参考。

虽然规范的特定部分没有WeakReference明确说明,但它不需要。就编译器而言,您只有一个指向该值的局部变量。

WeakReference只是另一个将对象作为参数的类;从编译器的角度来看;它没有理由相信(或以一种或另一种方式做出假设)该类是否保留它已通过的引用。考虑一下我是否有一个这样的类被使用:

public class MyClass
{
    public MyClass(object foo)
    {
        Console.WriteLine(foo);
    }
}

在我的代码中,我这样做了:

var relay = new Relay();
...
var myClass = new MyClass(relay);

我没有对我分配给 的值引入任何新的强引用relay,因为MyClass没有保留该引用。就编译器而言WeakReference,旨在为您提供对计为强引用的对象的引用的“特殊”类的事实是无关紧要的。

可达性不是由范围定义的;它由所讨论的变量(不是值)是否位于任何可能的未来代码路径中来定义。由于relay在函数后面没有以任何形式出现,因此变量(以及它对对象的引用)被认为是不可访问的并且有资格收集。这就是DisableOptimizations标志存在于程序集级别的原因,以便运行时知道(除其他外)要等到变量超出范围后才符合收集条件,以便调试器可以访问它。

于 2012-08-29T15:07:40.523 回答