Reputation: 170489
This question is basically why we need GC.KeepAlive()
in the first place.
Here's where we need it. We have a wrapper for some unmanaged resource
public class CoolWrapper
{
public CoolWrapper()
{
coolResourceHandle = UnmanagedWinApiCode.CreateCoolResource();
if (coolResourceHandle == IntPtr.Zero)
{
// something went wrong, throw exception
}
}
~CoolWrapper()
{
UnmanagedWinApiCode.DestroyCoolResource(coolResource);
}
public void DoSomething()
{
var result = UnmanagedWinApiCode.DoSomething(coolResource);
if (result == 0)
{
// something went wrong, throw exception
}
}
private IntPtr coolResourceHandle;
}
and our code uses that wrapper:
var wrapper = CoolWrapper();
wrapper.DoSomething();
and if this code is run in Release configuration and not under debugger then it may so happen that code optimizer sees that the reference is not actually used after this code and also that coolResourceHandle
member variable is not accessed (by managed code) after it was read inside DoSomething()
and its value was passed into unmanaged code and so the following happens:
DoSomething()
is calledcoolResourceHandle
is read~CoolWrapper()
runsUnmanagedWinApiCode.DestroyCoolResource()
runs and the resource is destroyed, the resource handle is invalidatedUnmanagedWinApiCode.DoSomething()
runs using the value which now refers to a non-existing object (or maybe another object is created and assigned that handle)The situation described above is actually possible and it's a race between a method of the object and a running garbage collection. No matter there's a local variable of reference type on stack - optimized code ignores that reference and the object becomes eligible for garbage collection immediately after coolResourceHandle
was read inside DoSomething()
.
So, to prevent this we use GC.KeepAlive()
:
var wrapper = CoolWrapper();
wrapper.DoSomething();
GC.KeepAlive(wrapper);
which makes the object non-eligible for GC until GC.KeepAlive()
is invoked.
This of course requires that all users use GC.KeepAlive()
everywhere which they will forget, so the right place is CoolWrapper.DoSomething()
:
public void DoSomething()
{
var result = UnmanagedWinApiCode.DoSomething(coolResource);
GC.KeepAlive(this);
if (result == 0)
{
// something went wrong, throw exception
}
}
and this basically prevents the objects from getting eligible for GC while there's a method of this object running.
Why is this needed? Why wouldn't GC ignore the objects which have a method running at that moment and also have a finalizer? This would make life much easier yet we need to use GC.KeepAlive()
instead.
Why is such aggressive collection allowed instead of ignoring objects which have methods currently running and a finalizer (and so likely to have problems in case there's a race as described above)?
Upvotes: 2
Views: 312
Reputation: 81179
Finalizers don't "collect" anything. Instead, they prevent objects from being collected and notify objects that they would have been collected but for the existence of an active finalizers. Note that if object X holds a reference to Y, Y will be uncollectable if either X or Y has an active finalizer. Y's finalizer (if it exists) will have no way of knowing whether it's the only thing keeping Y alive, or whether other finalizers may exist that would also keep Y alive.
A fundamental principle is that objects exist as long as any reference to them exists anywhere; as soon as the last reference to an object ceases to exist, the object will as well. The GC does not destroy objects; instead, it reclaims memory that was formerly used by objects that have ceased to exist. If an object has an active finalizer, a reference to it will be kept in a special list of objects that have active finalizer; as long as that reference exists, the object will do so as well. When a GC is performed, the system marks all the objects that would exist even in the absence of that list, and once that's done it produces a queue of objects that are on that list but haven't been marked. After that, it will start calling finalizers of objects on that queue.
Upvotes: 2
Reputation: 239704
Consider any method that creates garbage and then spends a long time doing other things before exiting. The obvious example is the main
method of any executable which may perform any number of initialization actions before entering some form of loop (such as a windows message loop) that won't exit for the entire lifetime of the process.
We want to be able to clean up that garbage. But that means we have to allow the GC to not treat methods as opaque - it has to be able to inspect a running method and know what is still in use right at this moment and only protect those items from being collected.
This is why the GC is "aggressive" and why object collection can happen at any time - even whilst the constructor is still running (assuming it will not access any instance members from the current point of its execution forwards).
Upvotes: 1
Reputation: 1500785
Why is this needed? Why wouldn't GC ignore the objects which have a method running at that moment and also have a finalizer?
Because that's not what the GC (or the C# specification) guarantees. The guarantee is that if an object won't be finalized or collected while it's still possible to read a field from it. If the JIT/GC detects that although you're currently executing an instance method, there's no execution path whereby that method will read any more fields, it is legal for the object to be collected (assuming there's nothing else keeping it alive).
It's surprising, but that's the rule - and I strongly suspect that the reason for it is to allow optimization paths that would otherwise be impossible.
Your fix of using GC.KeepAlive
is a perfectly reasonable one. Note that the number of situations where this is relevant is pretty tiny.
Upvotes: 5