The automatic mechanism cannot be deterministic, because it must rely on on the GC to discover whether the object is referenced or not. At times this behaviour is a show stopper. because temporary “resource leaks” or holding a shared resource locked for slightly longer than necessary might be unacceptable in an application. At others, it’s perfectly acceptable. I will focus on the scenarios where it is.
Any type can override protected Finilize method defined by System.Object to indicate that it required automatic finalisation. However the C# syntax for requesting automatic finalisation on a class A is to implement method ~A(). This method is called finiliser a must be invoked when the object is destroyed.
Incidentally any type can have a finalizer, even the value types. However the finalizer on the value type object will never be invoked.
When an object with finiliser is created, a reference to it is added to a special queue – called finalisation queue.
It should be noted that like any collection this queue requires a synchronisation, so creating many objects with finalizers from different threads will theoretically affect the performance.
When the object gets unreferenced in the application, only the finalisation queue will have a reference to that object. The GC then moves that reference to f-reachable queue. This queue is considered a root to that object, so the object cannot be collected just yet.
It’s worth pointing out that object’s finiliser doesn’t run together with GC, instead a special thread called finilizer is invoking the finalisation method on each of the objects. Once the finalizer method has been run it is then removed from the f-reachable queue, leaving it without any reference to it – so during the next iteration, the GC is free to reclaim memory from the object.
If the finalise method throws an exception, that exception is simply ignored.
Pitfalls of Non-Deterministic Finalisation
It should be obvious to anyone by now that this model carries significant performance penalties:
- Object with finalisers are guaranteed to reach at least Generation 1
- They are more expensive to allocated, since they have to be added to finalisation queue. Even more expensive if those objects are added from different threads and synchronisation is required
- Pressure on the finalizer thread might cause memory leaks, if the rate of allocation is greater that the rate at which finilizer thread is able to run.
What it means
Deterministic memory collection should be used in favour of finilizers. One of such patterns in .NET is IDisposable interface. Finalisers should be used as a last resort, in case the developer forgets to deterministically clean up after the object