Navigation: Programming Cookbook > Weak References and Finalization >
How do Finalization and Mourning Actually Work?
The identification and queuing of objects for finalization, and conversion of weak references to corpses, is a by-product of garbage collection (although there are some fairly significant modifications to the garbage collector to enable this "by-product"). The rules by which this process is governed are described below. Precisely when garbage collection is performed (and therefore when objects finally get a chance to carry out their last requests) is a policy decision made by the memory manager (and for the current beta release, this policy is rather simplistic). The memory manager is also responsible for administering last rites (sending #finalize) and bereavement counselling (sending #elementsExpired:) for those objects queued by the garbage collector.
The memory manager maintains a pair of processes for each of these tasks, referred to as the Finalizer and the Undertaker. The respective tasks of these two processes are to dequeue objects from the separate finalization and bereavement queues, and send them #finalize and #elementsExpired: messages. The main loop of these processes are very simple, for example:
"Private - Wait for objects to be queued to the system finalization queue, then attempt
to empty that queue. The VM signals the queue semaphore each time it queues a
group of objects to the queue so the semaphore's signal count does not reflect the
number of elements waiting in the queue (this is to reduce the asynchronous
signalling overhead and to permit user code to force synchronous finalization
by directly invoking administerLastRites). Should the finalizer be prematurely
terminated by an error occurring in a #finalize or #finalizeElements: method, then it
will be restarted the next time the system goes idle, but the semaphores signal count
will not be reset, so any waiting objects should get serviced in the near future,
assuming Processor time is available."
finalizer := Processor activeProcess.
finalizer name: 'Finalizer'.
[finalizer == Processor activeProcess] whileTrue: [
Communication between the memory manager's processes and the VM is achieved by the use of a pair of Semaphores which the VM signals whenever it places new elements in the queues. The VM only signals each semaphore once, no matter how many items it places in their respective queues, so the processes employ a secondary loop to dequeue objects until the queues are empty, for example:
"Private - Dequeue dying objects from the system finalization queue, and send them a
#finalize message. Multiple finalization processes are permissible because access to
the VM queue is atomic (which is in any case required for synchronisation with the
VM's garbage collection activities). This allows a user Process to synchronously
perform finalization at certain points, for example, so that all objects pending
finalization are processed before the image is saved. This may be necessary
because the Finalizer runs at a very low priority."
| dying |
[(dying := self dequeueForFinalization) isNil]
whileFalse: [dying finalize]
Objects dequeued for finalization, are not in any special state, so there are no restrictions on the operations which can be performed against them.
The finalizer runs at a priority one level above the base idle priority in order to minimize the impact of finalization on the foreground system processing. This can result in some delay before finalization if intensive processing is underway. However, as the comment in the method states, finalization can be forced at foreground priority at any time by simply sending #administerLastRites to the memory manager. For example, this is done when saving the image to ensure that objects requiring finalization are not saved down into the image:
"Private - Clean up before a snapshot. We perform a compacting garbage collect
to minimize the image size, and then move objects queued for finalization.
Two garbage collects are performed to ensure that any objects surviving the first
GC because they are referenced by objects pending finalization will be collected
by the compacting GC. We need to ensure that all objects pending finalization
do actually get finalized to ensure that those holding external resources
are cleared down, otherwise, when they are later finalized in a new session,
their internal state will be invalid (they will contain invalid handles).
It is not necessary to inform weak objects of their losses, because the Undertaker
runs at a high priority and should interrupt us to perform that task."
The undertaker, in contrast, runs at a very high priority (only just below the priority of the timing process which administers Delays). This is necessary because weaklings generally need to repair the damage sustained from the loss of constituents as soon as possible.
The memory manager monitors the state of its finalizer and undertaker whenever the system is about to go idle, and if it finds that either is not ready to run, or is not waiting on the appropriate semaphore, then it starts a new process. This is a defensive operation to try to prevent system melt down if finalization/mourning code either causes an exception, or inappropriately puts the process to sleep.