Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In .NET, can a finalizer be run even if an object's constructor never ran?

I understand that in .NET, finalizers are run even if an object is partially constructed (e.g. if an exception is thrown out of its constructor), but what about when the constructor was never run at all?

Background

I have some C++/CLI code that does effectively the following (I don't believe this is C++/CLI specific, but this is the situation I have at the ready):

try {
   ClassA ^objA = FunctionThatReturnsAClassA();
   ClassB ^objB = gcnew ClassB(objA); // ClassB is written in C# in a referenced project
   ...
}
catch (...) {...}

I have a 100% repeatable case where, if an exception is thrown out of FunctionThatReturnsAClassA(), and then a GC is triggered (seems to be reliably triggered by running this code again, but waiting a while also works), ClassB's finalizer is called.

Now, via trace output I can confirm that ClassB's constructor is not running (which is of course what you'd expect). So somehow, objB was apparently allocated and added to the finalizer list, before the preconditions for calling its constructor were even met (i.e. collecting the result from FunctionThatReturnsAClassA()).

This only happens in optimized release builds running outside the debugger. There are a variety of small changes I can make that result in the finalizer not running -- for instance inserting another method call between the two statements, or (tellingly, I think) moving the "gcnew ClassB" into a separate function that returns the object.

It seems to me that somehow the allocation part of the gcnew statement is getting reordered and run before the previous statement, but this reordering is NOT reflected in the generated MSIL code (defeating my initial assumption that this was just another C++/CLI code gen bug). Further, comparing the generated MSIL code between the "buggy" state and any of the "fixed" states shows no unexpected structural changes.

I've looked at the generated x86 code in the debugger as well and it doesn't look strange so far, but I haven't analyzed it as deeply and anyway I can't reproduce this behavior in the debugger so I'm not 100% sure the code I get from the debugger is the same as the code that shows the strange behavior.

So it could be an MSIL->x86 code gen quirk or it could be a processor instruction reordering (the former seems more likely but I haven't confirmed by trying harder to get the exact code in memory when the behavior occurs -- this is my next step).

Question

So is it valid (for lack of a better term) for the allocation of an object in .NET to be divorced and reordered separately from the constructor call for that object?

like image 425
rationull Avatar asked Dec 24 '15 04:12

rationull


People also ask

What is constructor and finalizer?

A constructor method is used to initialize an object, while finalizer methods are called just before the object is garbage-collected and its memory reclaimed. The syntax of the finalizer method is simply finalize(). The Object class defines a default finalizer method.

What is a finalizer in C#?

Finalizers (historically referred to as destructors) are used to perform any necessary final clean-up when a class instance is being collected by the garbage collector. In most cases, you can avoid writing a finalizer by using the System. Runtime. InteropServices.

What is the difference between destructor and finalizer in C #?

A destructor is run when the program explicitly frees an object. A finalizer, by contrast, is executed when the internal garbage collection system frees the object.

Why is it preferred to not use finalize for clean up?

Finalizers are time-limited and the GC will terminate a finalizer if it blocks for too long - this is the primary reason to avoid it where possible.


1 Answers

As covered in comments, the answer is "Yes" -- a finalizer can run if a constructor didn't run or didn't complete. However a finalizer cannot run if an allocation didn't occur (which is independent of the constructor call).

This is now confirmed to be a JIT optimization bug: https://github.com/dotnet/coreclr/issues/2478

like image 78
rationull Avatar answered Oct 03 '22 03:10

rationull