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

1

9

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?

Libbie answered 24/12, 2015 at 4:0 Comment(4)
It's possible for an object to be allocated without calling any of its constructors by calling FormatterServices.GetUninitializedObject(). Both BinaryFormatter and DataContractSerializer construct objects in this way. Later, if the object has a finalizer, it will get finalized. But I don't think that is what you are seeing, is it?Boresome
It's not exactly what I'm seeing but it does highlight that the framework can create objects this way and it's at least theoretically possible that something could get interrupted between the allocation and the construction. But, it doesn't answer whether it's legal for those two stages to be split apart when they are run as part of a standard new/gcnew statement.Libbie
JItter optimizer bugs can happen. But not this one, the object allocation (which gets the finalizer to run) and the constructor call is a single call to a CLR helper function. A single call cannot have re-ordering problems. You'll need a good repro with documented CLR version number and get on the telephone with Microsoft Support to get ahead.Castorina
That's what I'm afraid of. I can reproduce this in multiple CLR versions but this occurs in the middle of a large application codebase and I haven't been successfully reproduced it in a scratch project yet. I'm not optimistic about that.Libbie
L
2

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

Libbie answered 29/12, 2015 at 6:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.