Garbage Collection
Garbage collection is a process by which a run-time environment automatically
de-allocates and reclaims ununsed memory. Garbage collection
represents a trade-off between increased code robustness
and decreased code efficiency. By freeing the programmer of the obligation
to explicitly de-allocate memory, garbage collection avoids dangling references
caused by prematurely de-allocated memory and memory leaks caused by failing
to de-allocate memory when the last pointer to the object is obliterated.
Dangling references are among the most difficult and costly error to
detect, so eliminating them can considerably speed up code development and
increase the robustness of the code.
However, garbage collection requires a considerable amount of bookkeeping
and is famously associated with pauses in a program while garbage collection
is performed. Additionally, programmers typically know when an object is
no longer needed and can be de-allocated. A programmer can manually de-allocate
an object much more directly and efficiently than a garbage collector, thus
leading to faster code.
Garbage collection algorithms fall into two broad categories: reference
counting and tracing collection. The rest of these notes explores these
algorithms in more detail.
- Reference Counting
- Description: Keeps track of the number of pointers that
reference each block of memory in the heap and adds a block
of memory to the free list when its reference count drops to 0
- each memory block contains a four byte field which counts
the number of references to this block
- the compiler inserts statements that increment or decrement
reference counts around program statements that perform
pointer variable assignment
Example: Reference Counts
"Hello" "World"
String a = new String("Hello"); 1 0
String b = new String("World"); 1 1
b = a; 2 0
After the assignment of a to b, the string object
containing "World" gets garbaged collected
- Advantages
- the cost of garbage collection is distributed uniformly
over a program's execution so there is never an irritating
pause in program execution
- Disadvantages
- reference counting statements must be added before and
after every program statement that performs a pointer
assignment. These statements can increase the cost of
a pointer assignment by a factor of 3.
- reference counting cannot garbage collect circular
structures
- every block of memory must have an extra four bytes that
are devoted to reference counting.
- assumes a strongly typed language because the compiler
needs to know what to do with a pointer that references
a field in the middle of a memory block (i.e., it needs to
be able to generate code that finds the reference count
for the memory block and increments/decrements it
appropriately)
- Tracing Collectors: Tracing collectors work by marking objects as
either usable or un-usable and reclaiming un-usable objects
- Mark and Sweep
- Description: A three step method that is periodically
invoked to determine which objects in the heap are not
referenced by any chain of pointers that originates from
outside the heap (i.e., from a pointer currently on the
stack or from a global variable).
- Step 1: Walk through the heap and mark every block as
"useless"
- Step 2: For each pointer outside the heap do a depth-first
search and mark all blocks that are encountered as
"useful." (when the collector reaches a block already
marked as "useful" it knows it has reached the block
via some other path and can stop recursing) Doing a
depth-first search means having to recurse into structures.
For example, suppose we are given the following class
and variable declarations:
class ListNode { class List {
ListNode *next; ListNode *head;
ListNode *prev: ListNode *cursor;
int value; };
};
List *a;
When we follow a's pointer, we must recurse into List
and follow List's head and cursor pointers. Similarly
when we follow the head and cursor pointers, we will
have to recurse into ListNodes and follow their *next
and *prev pointers.
- Step 3: Walk through the heap again and move every block
still marked as "useless" to the free list
- Advantages
- requires less overhead than reference counting since it
is only called when free memory is exhausted
- garbage collects circular structures
- Disadvantages
- can cause long annoying pauses when garbage collection
occurs. Since garbage collection typically occurs when
free memory is exhausted, a great deal of memory must be
typically searched. Hence the garbage collection process
can take a noticeable period of time and can disrupt the
interactivity of an application. There are some incremental
garbage collectors that interleave their execution with
the program, thus amortizing the cost of their execution.
These garbage collectors lead to somewhat slower overall
execution times, because of thread switching and more
bookkeeping, but can dramatically reduce the length of
the pauses.
- requires a strongly typed language and each memory block
must start with a pointer to a type descriptor. The reason
a type descriptor is required is that if the object contains
pointer fields, then the garbage collector must recursively
follow these pointer fields, and the type descriptor will
tell the garbage collector where in the object these pointer
fields are located.
- the algorithms can be fairly tricky to implement. For
example, since garbage collection is initiated when free
memory is exhausted, there will not be a great deal of
space to store the stack required for depth-first search.
Hence many algorithms reverse the pointers as they
traverse the heap so that they can return to their
previous block when they finish recursing
- Stop-and-Copy
- Description: A technique that can eliminate memory fragmentation
by compacting storage. It divides the heap into two halves of equal
size and does all its allocation in one half. When this half fills up,
it copies the usable memory to the other half, compacts the storage,
and patches up pointers. Although it may seem that dividing the heap
in half cuts memory in half, that is not really the case since modern
processes use virtual memory that gets mapped to physical memory. Hence
you're really only halving virtual memory, which you can make quite
large using current disk sizes.
- Detailed Algorithm
- When a half is nearly full, the collector starts its
exploration of reachable data structures. Each reachable
block is copied into the second half of the heap, with no
external fragmentation
- The old version of the block, in the first half of the
heap, is overwritten with a "useful" flag and a pointer
to the new location.
- Any pointer that refers to the same block and that is
found later in the exploration is updated to point to the
new location.
- Advantages
- Eliminates memory fragmentation
- Eliminates steps 1 and 3 of mark-and-sweep
- Disadvantage: Halves the heap size, although as noted earlier,
it only really halves virtual memory heap size
- Generational Collectors
- Key Observation: Most dynamically allocated objects
are short-lived, so they can be placed in a "nursery"
region and garbage collected more quickly
- Detailed Description
- Memory is divided into multiple regions, often just
two, with one region being the nursery region and one
being the long-lived region
- When space runs low, the garbage collector garbage
collects the nursery region. If not enough space can
be reclaimed in the nursery region, it garbage collects
successively higher regions, thus weeding out older
lived blocks that have become un-usable.
- After some number of collections, frequently just one,
blocks get promoted from their region to the next older
region in a manner similar to stop-and-copy.
- One complication is that there may be pointers from
the older regions to the newer regions and we don't want
to exhaustively search the older regions to update
pointers when newer objects are promoted. To handle this
case, the compiler must generate code at each pointer
assignment to check whether the new pointer is an
old-to-new pointer. If so, it adds the pointer to a hidden
list accessible to the collector. This instrumentation
on assignment is known as a write barrier. In
general the list of old-to-new pointers is fairly short,
since it is relatively rare for an older object to point
to a newer object.
- Advantage: Decreases the length of pauses by garbage
collecting smaller regions
- Disadvantage: Much more complicated and can slow
down overall execution by requiring the instrumentation
of pointer assignments. However, the instrumentation is
pretty cheap. The nursery starts at a known region in
memory that ends with a bunch of 0's (e.g., 0x80000000)
so right shift the pointer to obtain the high order
bits of the pointer and then do a fast comparison. If
the right shifted pointer is less than the start of
the nursery, the pointer is into the older generations
and otherwise is into the nursery.