32位和64位应用程序中GC行为不一致

9

我注意到在使用VS 2013编译32位和64位的.NET 4.0控制台应用程序时,GC的行为不一致。

请看以下代码:

class Test
{
    public static bool finalized = false;
    ~Test()
    {
        finalized = true;
    }
}

Main() 中...

var t = new Test();
t = null;
GC.Collect();
GC.WaitForPendingFinalizers();
if (!Test.finalized)
    throw new Exception("oops!");

当以64位(调试)模式运行时,这个对象每次都能被成功回收;但是以32位模式运行时,即使我创建更多的对象并等待一段时间,也不能强制回收这个对象(我已经尝试过)。 有没有人有任何想法为什么会这样?这在调试必须处理32位程序集中释放未托管代理数据的代码时给我带来了麻烦。在32位模式下,有很多对象只是一直停留在那里,直到很长一段时间后才被回收(而64位模式不是这样)。 我正在尝试以32位模式调试某些内容,但终结器没有被调用(至少强制未调用)。对象只是停留在那里,永远不会被收集(我可以看到所有的弱引用仍然有值)。在64位模式下,所有的弱引用都被正确地清除,并且所有的终结器都被调用。注意:虽然上面的代码规模非常小,但在32位模式下,我发现许多对象都被卡在垃圾回收中,直到稍后创建更多对象(即使调用“Collect”和“WaitForPendingFinalizers”)。这在64位模式下从未发生过。我有一个用户想知道为什么会有这么多对象没有被收集,这促使我进行了调查,结果发现在64位模式下比32位模式更好。只是想理解为什么。编辑:这里是更好的代码以展示差异:
class Program
{
    class Test
    {
        public static bool Finalized = false;
        public int ID;
        public Test(int id)
        {
            ID = id;
        }
        ~Test()
        { // <= Put breakpoint here
            Finalized = true;
            Console.WriteLine("Test " + ID + " finalized.");
        }
    }

    static List<WeakReference> WeakReferences = new List<WeakReference>();

    public static bool IsNet45OrNewer()
    {
        // Class "ReflectionContext" exists from .NET 4.5 onwards.
        return Type.GetType("System.Reflection.ReflectionContext", false) != null;
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Is 4.5 or newer: " + IsNet45OrNewer());
        Console.WriteLine("IntPtr: " + IntPtr.Size + Environment.NewLine);

        Console.WriteLine("Creating the objects ...");

        for (var i = 0; i < 10; ++i)
            WeakReferences.Add(new WeakReference(new Test(i)));

        Console.WriteLine("Triggering collect ...");
        GC.Collect();

        Console.WriteLine("Triggering finalizers ..." + Environment.NewLine);
        GC.WaitForPendingFinalizers();

        Console.WriteLine(Environment.NewLine + "Checking for objects still not finalized ...");

        bool ok = true;

        for (var i = 0; i < 10; ++i)
            if (WeakReferences[i].IsAlive)
            {
                var test = (Test)WeakReferences[i].Target;
                if (test != null)
                    Console.WriteLine("Weak references still exist for Test " + test.ID + ".");
                ok = false;
            }

        if (ok)
            Console.WriteLine("All Test objects successfully collected and finalized.");

        Console.WriteLine(Environment.NewLine + "Creating more objects ...");

        for (var i = 0; i < 10; ++i)
            WeakReferences.Add(new WeakReference(new Test(i)));

        Console.WriteLine("Triggering collect ...");
        GC.Collect();

        Console.WriteLine("Triggering finalizers ..." + Environment.NewLine);
        GC.WaitForPendingFinalizers();

        Console.WriteLine(Environment.NewLine + "Checking for objects still not finalized ...");

        ok = true;

        for (var i = 0; i < 10; ++i)
            if (WeakReferences[i].IsAlive)
            {
                var test = (Test)WeakReferences[i].Target;
                if (test != null)
                    Console.WriteLine("Weak references still exist for Test " + test.ID + ".");
                ok = false;
            }

        if (ok)
            Console.WriteLine("All Test objects successfully collected and finalized.");

        Console.WriteLine(Environment.NewLine + "Done.");

        Console.ReadKey();
    }
}

它在64位系统上可以运行,但不支持32位。在我的系统上,“Test #9”永远不会被回收(弱引用仍然存在),即使创建更多的对象并尝试第二次。

顺便说一下:问这个问题的主要原因是因为我在控制台中有一个\gctest选项,用于在V8.Net和底层V8引擎之间测试垃圾收集。它在64位系统上可以运行,但不支持32位。


3
你尝试过在Test()函数中添加一些代码吗?也许空的finalizers被忽略了。 - user1023602
1
你是否在调试器中运行?它会执行很多操作以使调试更容易,包括扩展本地变量的生命周期。如果您在调试器之外运行,问题是否仍然显示?或者如果您退出该方法呢?您确定没有剩余的强引用吗? - Luaan
1
这就是有问题的代码,它非常相似,并且在32位模式控制台应用程序中失败。实际代码位于V8.Net测试控制台应用程序的“\gctest”选项中。 - James Wilkins
1
尝试将对象创建放在Main方法之外的单独方法中。通过一个人为的例子,你可以证明人为的问题。你确定终结器不会被优化掉吗?我的意思是,它什么也不做,你为什么要用它呢?在调试器下,变量的生命周期(即使是你看不到的临时变量)会延长到方法结束。 - Lasse V. Karlsen
1
32位和64位是不同的,调试器与32位和64位程序交互的方式也略有不同。您应该创建一个示例,展示不在调试器下运行的差异。 - Lasse V. Karlsen
显示剩余11条评论
1个回答

3
在 x86 和 x64 上,使用 VS2013 + .NET 4.5 没有重现此问题。
class Program
{
    class Test
    {
        public readonly int I;

        public Test(int i)
        {
            I = i;
        }

        ~Test()
        {
            Console.WriteLine("Finalizer for " + I);
        }
    }

    static void Tester()
    {
        var t = new Test(1);
    }

    public static bool IsNet45OrNewer()
    {
        // Class "ReflectionContext" exists from .NET 4.5 onwards.
        return Type.GetType("System.Reflection.ReflectionContext", false) != null;
    }

    static void Main(string[] args)
    {
        Console.WriteLine("Is 4.5 or newer: " + IsNet45OrNewer());
        Console.WriteLine("IntPtr: " + IntPtr.Size);

        var t = new Test(2);
        t = null;

        new Test(3);

        Tester();

        Console.WriteLine("Pre GC");
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Post GC");

        Console.ReadKey();
    }
}

请注意(如@Sriram Sakthivel所指出),您的变量可能存在“幽灵”副本,它们的生命周期会延长到方法结束时(当您在调试模式下编译代码或使用调试器附加时(F5而不是Ctrl + F5),您的变量的生命周期会延长到方法结束以便于调试)。
例如,对象初始化(当您执行类似于new Foo { I = 5 }的操作时)会创建一个隐藏的局部变量。

你展示的是不同的内容。你展示了一个具有隐藏本地变量的对象初始化器示例。你应该尝试使用OP的确切代码。它不会扩展到方法的末尾。 - Sriram Sakthivel
正如我之前所说,这并不等同于OP的代码。对象初始化器将创建一个未被设置为null的隐藏本地变量。因此,该对象是可访问的(通过隐藏本地变量)。对于OP的代码来说,这不是问题,变量将变得不可访问。事实上,我无法复现它,并投票关闭无法复现的问题。 - Sriram Sakthivel
@SriramSakthivel 你是对的。现在它是一个相反意义上的无法重现...调试器总是完成对象的最终处理。 - xanatos
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - xanatos
我已经强制将语言版本设置为3.0、4.0和5.0(在高级设置中),所有的x86版本都无法收集。 - James Wilkins
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接