In OCaml programs (and in most other programming languages), it is possible to
create
let x = [[1; 2; 3]; [4]] in let y = [2] :: List.tl x in y
Here the variable x
is bound to a list of two
elements, each of which is itself a list. Then the
variable y
is bound to a list that drops the first
element of x
and adds a different first element and the
function returns this new list. Since x
is never used
again that first list is inaccessible, or garbage.
Any boxed value created by OCaml can become garbage. This includes tuples, records, strings, arrays, lists and function closures as well as most user-defined data types.
Most garbage collectors are based on the idea of reclaiming whole blocks that
are no longer
Looking at memory more abstractly, we see that the memory heap is simply a directed graph in which the nodes are blocks of memory and the edges are the pointers between these blocks. So reachability can be computed as a graph traversal.
There are two basic strategies for dealing with garbage: manual storage management by the programmer and automatic garbage collection built into the language run-time system. Language constructs for manual storage management are provided by languages like C and C++. There is a way for the programmer to explicitly allocate blocks of memory when needed and to deallocate (or "free") them when they become garbage. Languages like Java and OCaml provide automatic garbage collection: the system automatically identifies blocks of memory that can never be used again by the program and reclaims their space for later use.
Automatic garbage collection offers the advantage that the programmer does not have to worry about when to deallocate a given block of memory. In languages like C, the need to manage memory explicitly complicates any code that allocates data on the heap and is a significant burden on the programmer, not to mention a major source of bugs:
In practice, programmers manage explicit allocation and deallocation by keeping track of what piece of code "owns" each pointer in the system. That piece of code is responsible for deallocating the pointer later. The tracking of pointer ownership shows up in the specifications of code that manipulates pointers, complicating specification, use, and implementation of the abstraction.
Automatic garbage collection helps modular programming, because two modules can share a value without having to agree on which module is responsible for deallocating it. The details of how boxed values will be managed does not pollute the interfaces in the system.
Programs written in OCaml and Java typically generate garbage at a high rate, so it is important to have an effective way to collect the garbage. The following properties are desirable in a garbage collector:
Fortunately, modern garbage collectors provide all of these important properties. We will not have time for a complete survey of modern garbage collection techniques, but we can look at some simple garbage collectors.
To compute reachability accurately, the garbage collector needs to be able to
identify pointers; that is, the edges in the graph.
Since a word of memory cells is just a sequence of bits, how
can the garbage collector tell apart a pointer from an integer? One simple
strategy is to reserve a bit in every word to indicate whether the value in that
word is a pointer or not. This
A different solution is to have the compiler record information that the garbage collector can query at run time to find out the types of the various locations on the stack. Given the types of stack locations, the successive pointers can be followed from these roots and the types used at every step to determine where the pointers are. This approach avoids the need for tag bits but is substantially more complicated because the garbage collector and the compiler become more tightly coupled.
Finally, it is possible to build a garbage collector that works even if you
can't tell apart pointers and integers. The idea is that if the collector
encounters something that looks like it might be a pointer, it treats it as if
it is one, and the memory block it points to is treated as reachable. Memory is
considered unreachable only if there is nothing that looks like it might be a
pointer to it. This kind of collector is called a
Mark-and-sweep proceeds in two phases: a
Marking for reachability is essentially a graph traversal; it can be
implemented as either a depth-first or a breadth-first traversal. One problem with a straightforward
implementation of marking is that graph traversal takes O(n) space where
n is
the number of nodes. However, this is not as bad as the graph traversal we considered earlier, one needs only a single bit per node in the graph if we modify the nodes to explicitly mark them as having been visited in the search. Nonetheless, if garbage collection is being performed because the system
is low on memory, there may not be enough added space to do the marking
traversal itself. A simple solution is to always make sure there is enough space
to do the traversal. A cleverer solution is based on the observation that there
is O(n) space available already in the objects being traversed. It is possible
to record the extra state needed during a depth-first traversal on top of the
pointers being traversed. This trick is known as
In the sweep phase, all unmarked blocks are deallocated. This phase requires the ability to find all the allocated blocks in the memory heap, which is possible with a little more bookkeeping information per each block.
When should the garbage collector be invoked? An obvious choice is to do it
whenever the process runs out of memory. However, this may create an excessively
long pause for garbage collection. Also, it is likely that memory is almost
completely full of garbage when garbage collection is invoked. This will reduce
overall performance and may also be unfair to other processes that happen to be
running on the same computer. Typically, garbage collectors are invoked
periodically, perhaps after a fixed number of allocation requests are made, or a
number of allocation requests that is proportional to the amount of non-garbage
(
One problem with mark-and-sweep is that it can take a long time—it has to
scan through the entire memory heap. While it is going on, the program is
usually stopped. Thus, garbage collection can cause long pauses in the
computation. This can be awkward if, for example, one is relying on the program
to, say, help pilot an airplane. To address this problem there are
Collecting garbage is nice, but the space that it creates may be scattered among many small blocks of memory. This external fragmentation may prevent the space from being used effectively. A compacting (or copying) collector is one that tries to move the blocks of allocated memory together, compacting them so that there is no unused space between them. Compacting collectors tend to cause caches to become more effective, improving run-time performance after collection.
Compacting collectors are difficult to implement because they change the locations of the objects in the heap. This means that all pointers to moved objects must also be updated. This extra work can be expensive in time and storage.
Some compacting collectors work by using an
A final technique for automatic garbage collection that is occasionally used
is
There are a few problems with this conceptually simple solution:
Applications for the Apple iPhone are written in Objective C, but there is no garbage collector available for the iPhone at present. Memory is managed manually by the programmer using a built-in reference counting scheme.
Generational garbage collection separates the memory heap into two or more
After an allocated object survives some number of minor garbage collection cycles, it is promoted to the tenured generation so that minor collections stop spending time trying to collect it.
Generational collectors introduce one new source of overhead. Suppose a program
mutates a tenured object to point to an untenured object. Then the
untenured object is reachable from the tenured set and should not be collected.
The pointers from the tenured to the new generation are called the
The goal of a garbage collector is to automatically discover and reclaim fragments of memory that will no longer be used by the computation. As mentioned, most objects are short-lived; a typical program allocates lots of little objects that are only in use for a short period of time, then can be recycled.
Most garbage collectors are based on the idea of reclaiming whole objects
that are no longer reachable from a
At an abstract level, a
For example, suppose memory looks like this, where the colored boxes represent different objects, and the thin black box in the middle represents the half-way point in memory.
Obj 1 | Obj 2 | Obj 3 | Obj 4 | Obj 5 |
At this point, we've filled up half of memory, so we initiate a collection. Old space is on the left and new space on the right. Suppose further that only the red and light-blue boxes (objects 2 and 4) are reachable from the stack. After copying and compacting, we would have a picture like this:
Obj 1 | Obj 2 | Obj 3 | Obj 4 | Obj 5 | Obj 2' | Obj 4' |
Notice that we copied the live data (the red and light-blue objects) into new space, but left the unreachable data in the first half. Now we can "throw away" the first half of memory (this doesn't really require any work):
Obj 2 | Obj 4 |
After copying the data into new space, we restart the computation where it left off. The computation continues allocating objects, but this time allocates them in the other half of memory (i.e., new space). The fact that we compacted the data makes it easy for the interpreter to allocate objects, because it has a large, contiguous hunk of free memory. So, for instance, we might allocate a few more objects:
Obj 2 | Obj 4 | Obj 6 | Obj 7 | Obj 8 |
When the new space fills up and we are ready to do another collection, we flip our notions of new and old. Now old space is on the right and new space on the left. Suppose now that the light-blue (Obj 4), yellow (Obj 6), and grey (Obj 8) boxes are the reachable live objects. We copy them into the other half of memory and compact them, throwing away the old data:
Obj 4 | Obj 6 | Obj 8 |
What happens if we do a copy but there's no extra space left over? Typically, the garbage collector will ask the operating system for more memory. If the OS says that there's no more available (virtual) memory, then the collector throws up its hands and terminates the whole program.
We have described a fairly simple take on garbage collection. There are many different algorithms, such as mark and sweep, generational, incremental, mostly-copying, etc. Often, a good implementation will combine many of these techniques to achieve good performance. You can learn about these techniques in a number of places—perhaps the best place to start is the Online GC FAQ.
OCaml uses a hybrid generational garbage collector that uses two separate
heaps, one for small objects (the
The Java 5 garbage collector is also a generational collector with three
generations. In the two youngest generations, a copying collector is used,
but the third and oldest generation is managed by a