COS 441 - Garbage Collection Methods - April 2, 1996

Stop and Copy

The idea behind Stop and Copy collection is to rearrange the heap so that all free space is contiguous. To do this we divide the head into two semi-spaces called to-space and from-space. Regular allocation occurs only in from-space, so at collection time, to-space is empty. We reserve space for a forwarding pointer in every word. The algorithm follows.

copy-object(R):
	if R[0] is forwarding pointer
		return R[0]
	else
		copy R to to-space at to-alloc-ptr
		R[0] <- to-alloc-ptr
		advance to-alloc-ptr
		return R[0]
Stop and copy compacts allocated data so that there is no fragmentation. This may also improve locality. But it requires twice as much memory and may copy large, long-lived objects repeatedly. This algorithm is O(live-data).

Generational Garbage Collection

How can we overcome the problem of repeatedly copying large long-lived objects? Empirically we observe that most allocated data dies young. The idea behind generational copying garbage collection is to use multiple generation spaces with the to-space of a younger generation equal to the from-space of an older generation. Collect from from-space 0 to to-space of generation 1. Collect from generation 1 from-space to generation 2 to-space, etc. Only the oldest generation needs its own to-space. Collect younger generations more frequently than older ones.

The invariant that must be maintained for generational copying collection is that records in generation i only point to records in generation i, i+1, ... The roots for generation i collection are therefore the pointers from generations 0 to i-1. If generations 0 to i-1 have just been collected, they are empty, hence the roots for generation i are just the machine registers. We must be careful when assignment occurs because it can lead to pointers from older to younger generations. To handle this we need to have a list of back pointers to older generations.

Ephemeral, Real-Time, Parallel

Ephemeral collection allows the mutator to run in parallel with the sweeping phase of a mark/sweep collector. Real-time collection minimizes or eliminates garbage collection pauses by performing some of the work of collection at each allocation. Parallel collection employs a second processor to collect the heap of a primary processor while the mutator is running, with only minimal synchronization between the mutator and collector. There are a host of other varieties of garbage collectors for different special purposes.

Conservation Collection

Both mark/sweep and stop and copy collection require the collector to be able to distinguish pointers from non-pointers. Typically implementations of garbage collected languages like Scheme and ML allocate one bit in every word to indicate whether the word is a pointer or not. Hence data representations used for garbage collected languages are often radically different from those used for C-like languages.

Suppose that we do not have any tag bits. Is it still possible to do garbage collection? Idea: if a word looks like a pointer (i. e. right address range, alignment, points into an allocated data page, ...) assume that it is a pointer. Then the set of records we identify as live may be too large, but this is ok. Conservative Mark-Sweep: Conservatively identify pointers, use mark-sweep collection with mark bits stored in a separate data structure. Works with minimal requirements on mutator, but collector must be able to identify all pointers. This requires that there be no pointer "swizzling" by the mutator (representing pointers in such a way that the collector cannot identify them). There are conservative mark-sweep collectors for C. This idea works because in practice few integers look like pointers. Conservative Copying: Must be careful not to move an integer that looks like a pointer.

Reference Counting

Idea: keep a count in each record of the number of records pointing to it. When a count reaches zero, put the record on the free list. Reference counting requires the mutator to explicitly discard records it no longer needs.

(allocate)      // return record with count 1
(set-box! a b)  // discard (unbox a), increment count of b
(discard a)     // decrement count of A and if 0, add a to free list
The problem with reference counting is that cycles are never reclaimed.

Stack Allocation

Many environment records and continuations frames are allocated in LIFO order. For those frames that have stack-like behavior use a stack instead. The cost to stack allocate N frames is O(N) and also O(N) to deallocate. With generational copying garbage collection it takes O(N) to allocate and O(live-data) << O(N) to deallocate. This leads to Appel's conclusion that garbage collection can be cheaper than stack allocation. So why use a stack? The constants in these asymptotic expressions may be much lower for the stack implementation. Also, it may improve locality and cache behavior. The jury is still out on whether stack-based implementations of mostly functional languages are faster.