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 about Value
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 to SomeOtherMethod()
. This is always possible in C++/CLI. In C# Value
could be of type IntPtr
.
- In the examples above, consider implementing and using
IDisposable
for SomeClass
instead of GC.KeepAlive()
, if you need a finalizer.
- You still need to use
GC.KeepAlive()
if you can’t change the implementation of SomeClass
.
- 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
).
CodeProject