WHIR: “corpses” in GC finalization design
A strange idea came to me today while thinking about garbage collector design. On priors, probably not original, but I don't remember seeing it before, so!
There's a couple of usual mechanisms for allowing the mutator to hook object reclamation in GCs. Finalizers are a traditional one, but they don't allow the mutator to interact with their timing easily, sometimes leading to exciting concurrency issues, and object resurrection during finalization seems to be a source of exciting complications in both GC and application design. Java has been on the path to deprecating Object.finalize
for a while now.
(I'll refer to the procedure that's meant to execute after an object becomes unreachable as the “hook”, below, to make it clearer that it's not always a finalizer in the above narrower sense.)
Then there's guardians, such as the ones in Guile; will executors in Racket seem similar. These accumulate registered objects that would otherwise have been collected, then allow the mutator to dequeue them for cleanup. This avoids the synchronization issues of finalizers, but they interact weirdly with weak references, and resurrection still feels thorny to me; Andy Wingo has written about some related issues.
Things get simpler if you don't allow the hook access to the original object. The most recent place I've run into this personally is in SBCL's sb-ext:finalize
, whose documentation warns that you shouldn't close over the object in the hook closure or else it will never get collected. Similarly, in Java, phantom references are now promoted over finalizers; they use a queueing mechanism similar to guardians but can't be dereferenced. The Cleaner class abstracts over this to provide an interface that handles the queue processing in the background; it too warns to not close over the original object in the hook.
But the problem with not being able to get access to the original object is that it's common to need some of the information from it in the hook. For instance, finalization of GC objects is often used to backstop cleanup of foreign resources, but the hook needs the underlying resource handle for that—and if the object also provides explicit cleanup, then the hook needs to know whether it's been done already or not, so duplicating the shared info once isn't enough. The main way I know of to handle this is to box the shared info in a sub-object that the hook closes over, but the most straightforward way to do that also results in extra indirection during normal access. Keeping two copies of the info and only propagating writes avoids indirection on read but is more awkward to implement correctly. The Cleanable objects that Cleaner gives back above promise that they'll only run once whether explicitly or implicitly called, which helps a lot with the ergonomics of the easier cases (you trigger the same path for implicit and explicit cleanup) but still involves jumping through hoops.
So: what if, instead, the GC met the mutator halfway? Rather than either allowing access to the original object during finalization (with attendant complexities around reachability and resurrection) or disallowing it completely (with attendant difficulties around sharing state with the hook), you'd allocate a corpse object at registration time, which the mutator never sees while the original object is live. At finalization time, the GC would automatically copy info from the now-dead object into the corpse and pass that to the hook, while not allowing the original object pointer to escape.
Choosing what to copy could be done in a few ways. I like the idea of being able to designate any subset of the slots (maybe a static subset per concrete type if that makes it easier to synthesize the corpse type), but that could be pretty hairy to provide depending on the details. Simpler alternatives would be to copy everything (but that's probably more than needed) or extract a single slot (but the application has to degrade to the sub-object case above if the data is more complicated). In the case of a single source slot that's a pointer or can be converted into a tagged immediate, you could just reuse the slot where you'd have stored the corpse reference and avoid taking an extra allocation.
What have I reinvented? And is this useful? Or is it just too complicated or otherwise inferior to current practice?
Addendum like half an hour later (sigh): aha, I've found a problem I didn't think of the first time! If any of the copied slots are heap references, they could still create resurrection issues indirectly, so you still have to make sure the sequence of operations of the collector doesn't trip over that. Phooey. That's not so bad I guess—you could queue all the copied stuff to be marked along the way, and I think maybe it'd mean that ‘good’ acyclic cases can avoid triggering weird weak reference interactions or requiring extra collection cycles as compared to the guardian/will-executor approach where the original pointer always escapes. You could check for the ‘bad’ case and raise a ruckus, or try to exclude it a priori using static type restrictions, but those could both get awkward.