The image below demonstrates what the JVM heap might look like after an application has been running for some time.

As we saw in the last article, the JVM’s heap is divided into 3 main areas – the permanent generation (purple), the old generation (yellow), and the young generation. The young generation area is further subdivided into Eden (green), and two survivor spaces (blue and orange).

The root set of an application is a set of objects that define the highest level references available to the application. The most common members of this set are any static references available to the application, as well as any local variable references to objects that have been allocated by the application itself.

An object on the heap is considered to be “live” if a path can be traced from a root set member to that given object; otherwise the object is considered “dead”. Live objects are said to have passed the “reachability” test, whereas dead objects have failed that test. Live objects are important and must be preserved, whereas dead objects are considered garbage, and must be reclaimed during the next garbage collection cycle.

In the image below, live objects are indicated by a bright brick pattern, while dead objects are represented using a washed out wavy pattern.

We will use this diagram for further exploration of the GC mechanism.

Garbage collectors can typically be described using these dimensions:

  1. Tracing garbage collectors
    This describes a collector that relies on the reachability test to determine which objects should be preserved, and which ones are garbage and so need to be reclaimed.
  2. Stop the World or Concurrent collectors
    Collectors often rely on the fact that no mutations of the heap may occur once the garbage collector has begun its operation. A Stop the World collector freezes all activity in the JVM until it has completed its cycle. On the other hand, a concurrent collector attempts to run the garbage collection process concurrently with the running application. Allowing concurrent operation ratchets up the complexity of its implementation significantly. In addition, such collectors may not be as aggressive at cleaning up garbage as Stop the World collectors.
  3. Compacting collectors
    As memory allocations and deallocations occur over the life of an application, the heap tends to get rather fragmented. This degrades the ability of the JVM to satisfy requests for large contiguous blocks of memory. Compacting collectors address this by defragmenting the heap – by moving live objects so that they are adjacent to each other, maximizing the contiguous free space available on the heap. This results in more efficient memory allocation.

    An alternative to compacting is to allow an object to span non contiguous blocks of memory. The tradeoff is increased implementation complexity and memory management overhead.

  4. Generational Collectors
    Most objects allocated by an application are short lived. Usually, these objects survive only for the duration of a single method call, before being eligible for reclamation.

    Further, the effort required to manage memory is directly proportional to the size of the memory block that needs to be managed. In other words, the smaller the area to be managed, the faster the collection will complete, thereby increasing the amount of CPU time available to the application itself.

    A generational collector takes advantage of both these principles.

    First, it divides the heap into multiple areas – to reduce the area that must be combed during a garbage collection cycle.

    Second, it ensures that each area holds objects of a given generation. The permanent generation hold objects that are relatively permanent; the old generation holds objects that are considered elderly; and the young generation holds objects that are relative newcomers (either newborn or middle aged). This ensures that most of the collection activity can be focused on the small area of the heap that contains the volatile young generation; with old generation collection being required only rarely.

Garbage collectors in the Java world, contain one or more of the above characteristics. For instance, the standard serial collector is a generational, tracing, Stop-the-World, and compacting, type of collector.

The Young Generation area is sub divided further into Eden and two Survivor spaces. At any given time, there is only one “active” survivor space (labeled “From”). The inactive space is labeled “To”. After each successful garbage collection cycle within the Young Generation, the active and inactive survivor spaces switch roles.

Let us assume that 2 new allocations (the green checkerboard pattern) are being requested by the application. If there is enough memory available in Eden, the allocation is made in Eden (as shown), and the application proceeds with no latency being experienced.

The diagram shows that the first allocation succeeded and now resides on the heap.

However, the second allocation request was larger than available space in the Eden! The JVM is unable to fulfill the request, and so it awakens the garbage collector to come to its aid. This results in a Young Generation collection cycle.

The Young Generation collector begins by performing the reachability test on all objects in Eden and in the currently active Survivor space (labeled From). The live objects are identified using the reachability test. Next, it begins copying the active objects out into either the inactive Survivor space (if the object is fairly new) or into the Old Generation area (if the object has survived a number of previous young generation collections). This results in the heap as shown:

A few things to note here:

  1. The newly allocated object is created in Eden (as we’d expect)
  2. The active and inactive Survivor spaces switch roles (and are re-labeled) at the end of the garbage collection cycle.
  3. The previous contents of the Young Generation [Eden + previous From] have now been copied over either to the inactive Survivor space or to the Old Generation area. Here, 2 middle-aged active objects (blue) from the previous From, and 2 newborn active objects (green) from Eden have been copied over to the inactive Survivor space; and 1 elderly active object (blue) from the previous From has been copied over to the Old Generation.
  4. The requested object allocation (checkerboard green) was now satisified from Eden, and it now resides there.
  5. Both Eden and the previously inactive survivor space (To) are nicely defragmented, with active objects on one side and free space on the other. (I’ve not shown the areas marked as garbage in these areas).

This is a fairly quick and painless collection operation, and even if the collector were to stop the world, the pause would be rather unnoticeable. Hence, a Young Generation collection is also termed a “Minor Collection”.

So, what happens when the Old Generation itself becomes full? Let’s reimagine our heap as shown below, with a new object allocation waiting to be satisfied.

In this case, a Young Generation collection is called for because there isn’t enough space available in Eden to satisfy this allocaiton request. However, unlike the earlier scenario, the promotion of the middle-aged object from the active Survivor space to being an elderly object in the old generation cannot proceed because there is no free space available in the Old Generation. In this case, the Young Generation collection is put on hold, while an Old Generation collection cycle is spun up to free up space in the Old Generation.

This results in what is termed a Major Collection, since this activity is expensive (given the size of the entire heap), and the pause suffered by the application (for Stop The World collectors) is noticeable.

However, in the end, the net result of the heap should be exactly as shown as the result of the Young Collection.

This ends part 2 of this series.

Continue on to part 3 >