Now that the basics of garbage collection are behind us, in this post I’ll discuss the Garbage Collectors that are available for us to choose from.
Before we go there, let’s first look at two key measures that will help us evaluate the available garbage collectors. The time spent by an executing application can be expressed as the sum of the time spent by the application in actually doing useful work, and the time that the application had to forcibly wait for garbage collection activities to complete.
This can be expressed as:
Total Execution Time = Useful Time + Paused Time
This brings us to our definitions:
This is defined as the percentage of total time that is not spent in garbage collection. I.e., [Useful Time] / [Total Execution Time]
This is defined as the average of all the [Paused Time] values over the execution of the application.
Latency is usually a major concern for highly interactive or real time applications, where a delay in processing is noticeable and potentially significant. On the other hand, for server side web applications, a bigger concern is throughput, since the latency introduced by garbage collection may be dwarfed by the latencies introduced by other contributors such as database or network access.
It is important to note that it is hard for a garbage collector to maximize both measures. For instance, throughput can be enhanced by using a very generously sized young generation (which reduces the frequency at which GC cycles are run). However, this can adversely impact latency when the garbage collection does occurs, as the time per garbage collection cycle is directly proportional to the size of the area of the heap being managed.
The following garbage collectors are available for our use, and can be configured using the appropriate JVM switches.
|Serial||Young||Stop The World, Copying Collector, Single GC thread. (works as described in the previous article).|
|Serial Old (MSC)||Old||Stop the World, Mark Sweep Compact (MSC), Single GC thread|
|Parallel Scavenge||Young||Stop the World, copying collector, multiple GC threads. Provides higher throughput by executing GC tasks in parallel with each other (but not the app).
Cannot run during concurrent phases of the CMS.
|Parallel New||Young||As Parallel Scavenge, but can run during the concurrent phases of the CMS|
|Parallel Old/ Parallel Compacting||Old||Similar to Parallel Scavenge, but operates on the old generation.
uses multiple GC threads to speed up the work of Serial Old (MSC).
STW collector, but higher throughput for old generation collections.
|Concurrent Mark-Sweep (CMS)||Old||Breaks up its work into phases, and executes most of its phases concurrently with the application thread – resulting in low latency. However, it introduces substantial management overhead and results in a fragmented heap.|
Selecting a Garbage Collector
It is important to note that you can install different collectors to manage each generation. For instance, to use the Parallel Scavenge collector for the Young Generation, and the Serial Old collector for the Old Generation, you would use the following switch:
|Switch||Young Generation||Old Generation|
|UseSerialGC||Serial||Serial Old (MSC)|
|UseParNewGC||ParNew||Serial Old (MSC)|
|UseConcMarkSweepGC||ParNew||CMS (mostly used)
Serial Old (used when concurrent mode failure occurs)
|+UseParallelGC||Parallel Scavenge||Serial Old|
|UseParallelOldGC||Parallel Scavenge||Parallel Old|
Performance Tuning Considerations
While tuning is largely trial-and-error, and is highly dependent on your particular environment and application needs, there are a few guiding principles that might be of help.
- An insufficient heap is the leading cause of garbage collection. This is particularly a problem for server side JVMs, especially at high loads. Hence, devote as much space to the heap as possible. Try allocating between 50-70% of the physical memory on the server to the JVM and see if it makes a difference.
- Set the initial and maximum heap sizes to the same value. You’re likely going to end up at your maximum value anyway – so why not make it easier on the JVM – and avoid having to gradual grow your heap? This eliminates the CPU cycles required to grow the heap.
- Set the Young Generation size appropriately. It has to be small enough (to avoid lengthy GC pauses), but big enough to accommodate a large number of transitory objects. Use the NewSize and MaxNewSize parameters wisely, and set the young generation to about 25% of the total heap. You can also use NewRatio to set the size of the young generation relative to the old generation.
- The Young Generation area must be set to less than half the total heap (see Reference  for details on the Young Generation guarantee).
- Use the default Garbage Collectors and attempt some of the more complex options only if the situation warrants it.
- Ensure that you clear out references that are no longer needed. Pay particular attention to collections (such as maps) that may continue to hold obsolete references to objects, long after their usefulness has ended.
- Use JVM options like -verbose:gc, and -XX:+PrintGCDetails to monitor GC performance. Ideally, you want to avoid a sawtooth pattern, which large amounts of memory being freed up after each collection.
With this, I’ve come to the end of the story I set out to tell about garbage collection in Java. This question was prompted by an attendee at my presentation at SuperValu, Inc. in Chanhassen.
Do add a comment here if you find anything here that merits correction.