Today, while browsing some C++/CLI code, I stumbled upon several calls to GC.KeepAlive(someObj)
.
Immediately I thought memory leak – because I thought KeepAlive()
would keep the object alive indefinitely.
Fortunately, this turned out to be wrong, though.
After reading the documentation of GC.KeepAlive() (couldn’t really figure it out), I did some decompiling and found out that GC.KeepAlive()
looks like this:
1 2 3 4 |
[MethodImpl(MethodImplOptions.NoInlining)] public static void KeepAlive(object obj) { } |
It just does nothing. So what’s its purpose?
It’s there to fake an access to a variable.
Why? The .NET garbage collector may collect a variable directly after its last use – and not necessarily, contrary to common belief, at the end of the variable’s scope.
Consider this code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class SomeClass { // This field is initialized somewhere // in the constructor (not shown here). public SomeOtherClass Value; ... } ... void MyMethod() { SomeClass obj = new SomeClass(); SomeOtherMethod(obj.Value); YetAnotherMethod(); // obj still alive here? Possibly not. } |
The garbage collector may collect obj
just after the runtime has retrieved obj.Value
(line 15), i.e. before SomeOtherMethod()
is even called.
Note: The exact line where obj
will be marked for collection is up to the JIT compiler. The behavior describe above seems to be called “lookahead optimization”.
Usually this optimization not a problem. It becomes a problem, however, if SomeClass
has a finalizer:
1 2 3 4 5 6 7 8 9 10 11 |
class SomeClass { public SomeOtherClass Value; ~SomeClass() { // "Value" can't be used anymore // after Dispose() has been called. this.Value.Dispose(); } } |
So, if obj
‘s finalizer is executed before SomeOtherMethod()
is called, SomeOtherMethod()
won’t be able to use obj.Value
anymore.
To solve this problem, add GC.KeepAlive()
after the call to SomeOtherMethod()
, like this (line 5):
1 2 3 4 5 6 7 |
void MyMethod() { SomeClass obj = new SomeClass(); SomeOtherMethod(obj.Value); GC.KeepAlive(obj); YetAnotherMethod(); } |
This way, the garbage collector won’t collect obj
(and thus run its finalizer) before line 5 has been reached.
Notes:
-
The implementation of the finalizer of
SomeClass
is flawed – as the examples in this article show. The user shouldn’t need to worry aboutValue
being disposed too early.- Rule of thumb: A finalizer should only dispose the resources of its own class, not resources of some member (by calling
Dispose()
on members). - The problem with the finalizer persists if
Value
is an unmanaged resource/pointer that’s being passed toSomeOtherMethod()
. This is always possible in C++/CLI. In C#Value
could be of typeIntPtr
. - In the examples above, consider implementing and using
IDisposable
forSomeClass
instead ofGC.KeepAlive()
, if you need a finalizer. - You still need to use
GC.KeepAlive()
if you can’t change the implementation ofSomeClass
.
- Rule of thumb: A finalizer should only dispose the resources of its own class, not resources of some member (by calling
- Using
GC.KeepAlive()
is like using GCHandle, just more light-weight and faster. -
GC.KeepAlive()
only works because it can’t be inlined by the compiler (MethodImplOptions.NoInlining
).