Garbage Collection and Finalizers: Finer Points
Asked Answered
C

3

16

In answering another question* on SO, and the subsequent comment discussion, I ran into a wall on a point that I'm not clear on.

Correct me on any point where I'm astray...

When the Garbage Collector collects an object, it calls that object's finalizer, on a separate thread (unless the finalizer has been suppressed, e.g. through a Dispose() method). While collecting, the GC suspends all threads except the thread that triggered the collection (background collection aside).

What isn't clear:

  1. Does the Garbage Collector wait for the finalizer to execute on that object before collecting it?
  2. If not, does it un-suspend threads while the finalizer is still executing?
  3. If it does wait, what happens if the finalizer runs into a lock being held by one of the suspended threads? Does the finalizer thread deadlock? (In my answer, I argue that this is bad design, but I could possibly see cases where this could happen)

* Link to the original question:
.NET GC Accessing a synchronised object from a finalizer

Civism answered 7/3, 2011 at 18:0 Comment(0)
W
54

Does the Garbage Collector wait for the finalizer to execute on that object before collecting it?

Your question is a bit ambiguous.

When the GC encounters a "dead" object that needs finalization, it abandons its attempt to reclaim the dead object's storage. Instead, it puts the object on a queue of "objects that I know need finalization" and treats that object as alive until the finalizer thread is done with it.

So, yes, the GC does "wait" until the finalizer is executed before reclaiming the storage. But it does not wait synchronously. It sounds like you're asking "does the GC synchronously call the finalizer right there?" No, it queues up the object to be finalized later and keeps on truckin'. The GC wants to quickly get through the task of releasing garbage and compacting memory so that the program proper can resume running ASAP. It's not going to stop to deal with some whiny object that is demanding attention before it gets cleaned up. It puts that object on a queue and says "be quiet and the finalizer thread will deal with you later."

Later on the GC will check the object again and say "are you still dead? And has your finalizer run?" If the answer is "yes" then the object gets reclaimed. (Remember, a finalizer might make a dead object back into a live one; try to never do that. Nothing pleasant happens as a result.)

Does it un-suspend threads while the finalizer is still executing?

I believe that the GC thaws out the threads that it froze, and signals the finalizer thread "hey, you've got work to do". So when the finalizer thread starts running, the threads that were frozen by the GC are starting up again.

There might have to be unfrozen threads because the finalizer might require a call to be marshalled to a user thread in order to release a thread-affinitized resource. Of course some of those user threads might be blocked or frozen; threads can always be blocked by something.

what happens if the finalizer runs into a lock being held by one of the suspended threads? Does the finalizer thread deadlock?

You betcha. There's nothing magic about the finalizer thread that prevents it from deadlocking. If a user thread is waiting on a lock taken out by the finalizer thread, and the finalizer thread is waiting on a lock taken out by the user thread, then you've got a deadlock.

Examples of finalizer thread deadlocks abound. Here's a good article on one such scenario, with a bunch of links to other scenarios:

http://blogs.microsoft.co.il/blogs/sasha/archive/2010/06/30/sta-objects-and-the-finalizer-thread-tale-of-a-deadlock.aspx

As the article states: finalizers are an extremely complex and dangerous cleanup mechanism and you should avoid them if you possibly can. It is incredibly easy to get a finalizer wrong and very hard to get it right.

Wellbred answered 7/3, 2011 at 18:27 Comment(5)
+1 finalizers are an extremely complex and dangerous cleanup mechanism and you should avoid them if you possibly canLuciano
"and treats that object as alive until the finalizer thread is done with it" in that context doesn't the finalizer queue just count as another reference to that object, so it would fall under the normal rules of only GC'ing objects that are not referenced anymore?Interflow
For the question "what happens if the finalizer runs into a lock being held by one of the suspended threads?", isn't the answer "the GC unsuspends all of the threads when it's done collecting"?Sunny
@BrokenGlass: Correct; the finalizer queue is just another "guaranteed to be alive" root object. When the finalizer thread removes the object from the queue then the GC will once again eventually discover that it is dead, unless of course as I said the finalizer resurrected the object by copying a reference to it into, say, a static field.Wellbred
@Gabe: Right, the GC thaws out the threads that the GC froze. I interpreted the question as the more general question of "what happens when the finalizer thread needs to access a resource that has been locked by a suspended/blocked thread?" It really doesn't matter who suspended it; that's going to give you a deadlock.Wellbred
D
4

Objects that contain a finalizer tend to live longer. When, during a collect, the GC marks an object with a finalizer as being garbage, it will not collect that object (yet). The GC will add that object to the finalizer queue that will run after the GC has finished. Consequence of this is that, because this object is not collected, it moves to the next generation (and with that, all objects it refers to).

The GC suspends all running threads. The finalizer thread on the other hand will run in the background while the application keeps running. The finalizer calls all finalize methods on all objects that are registered for finalization. After the finalizer method on an object has ran, the object will be removed from the queue, and from that point on the object (and possibly all objects it still references) is garbage. The next collection that cleans objects of the generation of that object will (at last) remove that object. Since objects that live in generation 2 are collected about 10 times as less as objects that live in generation 1, and gen 1 ten times as less as gen 0, it can take some time for such object is finally garbage collected.

Because the finalizer thread is just a simple thread that runs managed code (it calls the finalizers), it can block and even dead lock. Because of this it is important to do as little as possible in finalize methods. Because the finalizer is a background thread, a failing finalize method could even bring down the complete AppDomain (yuck!).

You could say that this design is unfortunate, but if you think about it, other designs where the framework cleans our mess effectively, are hard to imagine.

So, to answer your questions:

  1. Yes, only after the object is removed from the finalizer queue, the object will be garbage and the GC will collect it.
  2. The GC suspends all threads, even the finalizer queue.
  3. The finalizer queue can deadlock. Lock as little as possible inside finalize methods.
Dric answered 7/3, 2011 at 18:30 Comment(1)
I didn't mean the design of the Garbage Collector was unfortunate... the design of the original program in question is unfortunate : )Civism
G
3

It's simplest to think of the garbage collector as dividing objects into four groups:

  1. Those which aren't reachable by any rooted object;
  2. Those which are reachable from a list of live finalizable objects, but not from any other rooted object;
  3. Those which are on the list of live finalizable objects, but are also reachable through some rooted object other than that list.
  4. Those which are not on the list of live finalizable objects, but are reachable via some rooted object other than that list.

When the garbage-collector runs, objects of type #1 disappear. Objects of #2 get added to a list of objects needing imminent finalization and removed from the "live finalizable objects" list (thus becoming objects of category #4). Note that the list of objects needing finalization is a normal rooted reference, so objects on this list cannot be collected while they are on it, but if no other rooted reference is created by the time the finalizer is complete the object will move to category #1.

Geyser answered 7/3, 2011 at 18:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.