Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the existence of a try/finally block stop the garbage collector from working?

Just let me start off with a demonstration:

[TestMethod]
public void Test()
{
    var h = new WeakReference(new object());
    GC.Collect();
    Assert.IsNull(h.Target);
}

This code works as expected. After garbage collection is over, the reference in h is nullified. Now, here's the twist:

[TestMethod]
public void Test()
{
    var h = new WeakReference(new object());
    GC.Collect();
    try { }      // I just add an empty
    finally { }  // try/finally block
    Assert.IsNull(h.Target); // FAIL!
}

I add an empty try/finally block to the test after the GC.Collect() line and lo, the weakly referenced object is not collected! If the empty try/finally block is added before the GC.Collect() line, the test passes though.

What gives? Can anyone explain how in exactitude does try/finally blocks affect the lifetime of objects?

Note: all testing done in Debug. In Release both tests pass.

Note 2: To reproduce the app must be targeting either the .NET 4 or the .NET 4.5 runtime and it must be run as 32-bit (either target x86, or Any CPU with "Prefer 32-bit" option checked)

like image 395
Stefan Dragnev Avatar asked Sep 05 '13 09:09

Stefan Dragnev


People also ask

What is the purpose of finally?

Why finally Is Useful. We generally use the finally block to execute clean up code like closing connections, closing files, or freeing up threads, as it executes regardless of an exception. Note: try-with-resources can also be used to close resources instead of a finally block.

What is a garbage collector and how does it work?

In the common language runtime (CLR), the garbage collector (GC) serves as an automatic memory manager. The garbage collector manages the allocation and release of memory for an application. Therefore, developers working with managed code don't have to write code to perform memory management tasks.

Why is garbage collector slow?

GC is slow, mostly because it needs to pause program execution to collect garbage. Think of it like this — your CPU can only work on one thing at a time. With C++, it's always working on your code, including the bits that delete memory.


1 Answers

When a debugger is attached, the jitter changes the lifetime of local variables. Explained in detail in this answer. Briefly, without a debugger the lifetime ends at the last use of the variable in the code, with a debugger it is extended to the end of the method to allow a Watch debugger expression to work.

While it looks like the new object() expression doesn't get stored in a variable in your code, there still is one after the jitter code generator is done with it. The object reference is stored on the stack frame at [ebp-44h], indistinguishable from the way a local variable would be used. The only way you can see that is by looking at the generated machine code, use Debug + Windows + Disassembly. This is otherwise entirely normal, these kind of redundant memory stores are eliminated by the jitter optimizer but it is not enabled in the Debug build.

Even though it is a temporary, this variable still needs to be reported to the GC as storing a reference. Necessary to prevent the object from getting collected when a GC occurs right between the object constructor call and the WeakReference constructor call. Possible if another thread in the program triggers a collection.

Without the try/finally blocks, the jitter can still discover that the stack frame slot stores a temporary and there isn't actually a need to extend its lifetime. So it stops reporting the lifetime of the temporary before the GC.Collect() call and the object gets collected.

But with the try/finally blocks, the jitter gives up trying to figure out if there is a possible usage of the stack frame slot in the try or finally blocks. And punts the problem by simply extending its lifetime to the end of the method, as would happen with a normal local variable.

This is all rather normal, you simply cannot make any reasonable assumptions about the way local variable references get treated in non-optimized code. This ought to also be a strong warning to anybody that actually uses a [TestMethod] that's run in a unit tester, never ever test the Debug build of code, only the Release build. It just won't behave the same as the way it works on the user's machine.

like image 99
Hans Passant Avatar answered Oct 21 '22 10:10

Hans Passant