9

I have noticed inconsistent behavior from the GC when compiling console applications under both 32-bit and 64-bit in .Net 4.0 using VS 2013.

Consider the following code:

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

and in Main() ...

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

When running in 64-bit (debug) mode this works every time without fail; however, running in 32-bit mode I cannot force this object to get collected (even if I create more objects and wait a period of time, which I have tried).

Does anyone have any ideas as to why this is? It's causing me trouble when trying to debug some code that must deal with releasing unmanaged proxy data for the 32-bit version of the assemblies. There's a lot of objects in 32-bit mode that just sit there until a long time later (no so in 64-bit).

I'm trying to debug something in 32-bit mode, but the finalizers are not getting called (at least, not by force). The objects just sit there and never get collected (I can see all the weak references still having a value). In 64-bit mode, all the weak references are cleared as expected, and all finalizers get called.

Note: Though the code above is on a very small scale, I have noticed in 32-bit mode many more objects stuck in the GC until more objects get created later (even when "Collect" and "WaitForPendingFinalizers" is called). This is never the case in 64-bit mode. I have one user wondering why so many objects where not getting collected, which caused me to investigate, to which I found out that everything seems to work better in 64-bit mode than 32. Just trying to understand why.

Edit: Here is better code to show the differences:

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();
    }
}

It works in 64-bit, but not in 32-bit. On my system, "Test #9" never gets collected (a weak reference remains), even after creating more objects, and trying a second time.

FYI: The main reason for asking the question is because I have a \gctest option in my console to test garbage collection between V8.Net and the underlying V8 engine on the native side. It works in 64-bit, but not 32.

leppie
  • 115,091
  • 17
  • 196
  • 297
James Wilkins
  • 6,836
  • 3
  • 48
  • 73
  • 3
    Have you tried adding a bit of code to ~Test()? Perhaps empty finalizers are being ignored somehow. –  May 15 '15 at 08:14
  • Yes, this is a simplified test version of the original. As well, optimization is not enabled. – James Wilkins May 15 '15 at 08:50
  • 1
    Are you running in a debugger? It does a lot of stuff to make debugging easier, including extending the lifetime of locals. Does the problem still show if you run outside of a debugger? Or if you exit the method? Are you sure there's no remaining strong reference? – Luaan May 15 '15 at 08:53
  • You need to show us a code which reproduces the problem. I can't reproduce the problem with the code provided. Can you? – Sriram Sakthivel May 15 '15 at 08:58
  • I am using VS2013, and running in debug mode. I haven't tried it outside of that yet. – James Wilkins May 15 '15 at 09:00
  • 1
    That's IS the code that has the problem, it's much the same, and fails in a 32-bit mode console app. The actual code is in the "\gctest" option of the V8.Net test console app. – James Wilkins May 15 '15 at 09:02
  • @JamesWilkins It is much the same isn't helpful. Can you reproduce the problem with this code provided? I mean with the `Test` class? If not, you'll have to create another code sample which reproduces the problem. – Sriram Sakthivel May 15 '15 at 09:03
  • Yes I can, otherwise I would not have posted it. ;) – James Wilkins May 15 '15 at 09:06
  • I'm also unable to reproduce the issue. So can you please create a [MCVE](http://stackoverflow.com/help/mcve) so that we can help you? – Damien_The_Unbeliever May 15 '15 at 09:07
  • 1
    Try doing the object creation in a separate method from Main. With a contrived example you can prove contrived problems. Are you sure that the finalizer is not just optimized away? I mean, it does *nothing*, why would you even have it? Under the debugger, the lifetime of variable (even temporary variables you don't see) is extended until the end of the method. – Lasse V. Karlsen May 15 '15 at 09:07
  • If it were optimized away in 32-bit, then why not in 64? ;) – James Wilkins May 15 '15 at 09:11
  • 1
    32-bit and 64-bit *are* different and the way a debugger interacts with a 32-bit and 64-bit program are also slightly different. You should create an example that shows the difference without running under the debugger. – Lasse V. Karlsen May 15 '15 at 09:13
  • Is this question out of curiosity? Because GC behavior is not guaranteed you can't rely on anything that you find to be true by testing anyway. Could change at any time. – usr May 15 '15 at 09:18
  • It causes a lot of objects to not get finalized - and objects not finalized means a lot of native side stuff still not handled, and makes debugging less desirable than it already is. ;) – James Wilkins May 15 '15 at 09:21
  • OK, if you want a workaround: Don't store the ref to Test in a local. Store it in a heap-based wrapper class such as `class Box { public T Value; }`. Given the current JITs it should be more likely that you are seeing deterministic reference behavior doing that. (No guarantees - but I think you understand that.) – usr May 15 '15 at 09:39
  • There is no inconsistency - the GCs aren't supposed to be identical nor *can* they be. The 64-bit GC has to deal with a whole lot more memory than the 32-bit GC and types that are twice as big. The 32 and 64-bit GCs are different just as the client and server GCs are different. Some of those differences are significant - the 64 bit runtime allows for tail-recursion optimizations when the 32 bit runtime throws a StackOverflowException. – Panagiotis Kanavos May 15 '15 at 10:16

1 Answers1

3

No repro on VS2013 + .NET 4.5 on x86 and x64:

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();
    }
}

Note that (as noticed by @Sriram Sakthivel), there could be "ghost" copies of your variable around, that have their lifespan extended until the end of the method (when you compile the code in Debug mode or you have a debugger attached (F5 instead of Ctrl+F5), the lifespan of your variables is extended until the end of the method to ease debugging)

For example object initialization (when you do something like new Foo { I = 5 }) creates an hidden local variable.

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • What you're showing is different. You show an example of object initializer which has the hidden local. You should try OP's exact code. It will not be extended to the end of the method. – Sriram Sakthivel May 15 '15 at 08:49
  • As I said earlier, it is not equal to OP's code. Object initializer will create a hidden local which is not being set to null. So the object is reachable(through the hidden local). With OP's code it is not a problem, variable will go unreachable. In fact, I can't reproduce it and voted to close as can't reproduce. – Sriram Sakthivel May 15 '15 at 08:54
  • @SriramSakthivel You are right. Now it is a no-repro in the opposite sense... The debugger always finalizes the objects. – xanatos May 15 '15 at 09:01
  • @JamesWilkins Even targetting the .NET 4.0, the runtime it will use is 4.5 . (but just to be sure I've checked it) I've modified the code to show if it is using .NET 4.5 or 4.0 . Sadly I don't have any machine with Visual Studio without .NET 4.5 – xanatos May 15 '15 at 09:16
  • I have forced the language version to 3.0, 4.0, and 5.0 (under advanced settings), and all in x86 fail to collect. – James Wilkins May 15 '15 at 09:25
  • Your code (with a lot more going on) does work. "Is 4.5 or newer" is true, and IntPtr is 4. However, the finalization order is different between bit versions (not that it should be in any order I know). – James Wilkins May 15 '15 at 09:53
  • @JamesWilkins .NET 4.5 is a binary replacement for .NET 4.0. If you've installed the 4.5 runtime on your machine, you are using the .NET 4.5 binaries. And the GCs *are* different because they target different CPU architectures, CPU commands, memory and type sizes – Panagiotis Kanavos May 15 '15 at 10:19
  • By the way, you can't claim "No repro" when your code is different from the posted code. – James Wilkins May 15 '15 at 14:22