When you compile .NET code for Release target, the garbage collector is really aggressive, that is, it has the potential to be.
Take this example:
public void Test()
{
FileStream stream = new FileStream(....);
stream.Write(...);
SomeOtherMethod();
}
In this example, once the call to Stream.Write
has returned, this method has no more use for the stream
variable, and is thus considered out of scope, which means that the FileStream
object is now eligible for collection. If the garbage collector runs during the call to SomeOtherMethod
that stream object might get collected.
Edit: As @Greg Beech points out in the comments, an object can be collected even if an instance method is currently executing on it.
For instance, let's say the code in FileStream.Write
looks like this:
public void Write(byte[] buffer, ...)
{
IntPtr unmanagedHandle = _InternalHandleField;
SomeWindowsDll.WriteBuffer(unmanagedHandle, ...);
...
here, the method first grabs the internal field containing an unmanaged handle. The object will not be collected before this has happened. But, if this is the last field of the object, or the last instance-method of the object, being accessed in Write, before transitioning over to just P/Invoke calls, then the object is now eligible for collection.
And since FileStream probably has a unmanaged file handle (I say probably, I don't know), it probably has a finalizer as well, so the unmanaged file handle is in risk of being closed before the call to WriteBuffer has completed. That is, if the supposed implementation of .Write
is as above, which it most likely isn't.
If you wish to prevent this, make the stream object live for the duration of the call to SomeOtherMethod
for whatever reason, you need to insert code after the call that "uses" the object in some way. If you don't want to call a method or read a property on the object, you can use the artificial "usage" method of GC.KeepAlive
to do it:
public void Test()
{
FileStream stream = new FileStream(....);
stream.Write(...);
SomeOtherMethod();
GC.KeepAlive(stream);
}
The method does nothing, but it has been flagged with attributes so that it won't be optimized away. This means that the stream variable is now in use for the duration of the call to SomeOtherMethod
and the FileStream
object stored in it will not get collected during that time.
That's all it does. GC.KeepAlive
leaves no permanent mark on anything, it does not alter the lifetime of the object after being called, it simply adds code that "uses" the variable at some point, which prevents the garbage collector from collecting the object.
I learned about this method, or rather, the point of this method, the hard way with some code that looked like this:
public void Test()
{
SomeObjectThatCanBeDisposed d = new SomeObjectThatCanBeDisposed();
SomeInternalObjectThatWillBeDisposed i = d.InternalObject();
CallSomeMethod(i);
}
Notice that I construct an instance of an object implementing IDisposable
, and in the original case, it also implemented a finalizer. I then "fished" out an object it kept track of, and the way the disposable object was set up (incorrectly) was that once the finalizer ran, it would also dispose of internal objects (which is bad). In the above code, d becomes eligible for collection once the internal object has been extracted, and once the object is collected, it finalizes itself by disposing of the object I extracted. This means that during the call to CallSomeMethod
, the object passed to the method died and I couldn't understand why.
Of course, the fix for the above code was not to use GC.KeepAlive
, but instead to fix the incorrect finalizer, but if the "internal object" had been an unmanaged resource, things might've been different.
Now, as for your keyboard hook, if you store your reference in a root, like a static field, a member field of an object that is also kept alive, etc. then it should not be collected out of the blue.