Incorrect GC Strategy and Configuration
Often applications rely on the default settings and behavior of the JVM for garbage collection. Unfortunately while some JVMs, like Oracle JRockit, attempt to dynamically deploy the best strategy, they cannot detect whether your application cares more about response time or throughput. Default garbage-collection settings are a performance compromise to make sure that most applications will work, but such settings are not optimized for either type of application. Don’t expect many of your applications to run their fastest and jump their highest without some careful testing and analysis on your part.
Performance of both response time-bound and throughput-oriented applications suffers from frequent garbage collection; however, the strategies for fixing this problem are different for the two kinds of application. Determining the correct strategy and sizing will allow you to remedy the majority of GC-related performance problems, so here are some tools and tactics for finding optimal solutions to the most common GC issues:
- To optimize for response time, use the GC suspensions monitor (or activations monitor if it’s all that’s available). Always use a parallel young-generation collector to ensure short minor GCs (remember they still suspend everything). This will ensure that not more than a few temporary objects get tenured to the old generation. If you cannot prevent an overflow to the old generation, you might require concurrent old-generation garbage collection to avoid longer suspensions (See the Tuning section earlier in this chapter).
- To optimize for throughput, it is important to make the GC as fast as possible. Weâve discussed how longer suspensions due to high concurrency lead to exponentially worse throughput. Utilizing all of the CPUs in parallel will usually yield the shortest suspensions, which is why you want to choose a parallel collector for the young and the old generation. In case you have a lot of CPUs and the old-generation GCs still take too long (several hundred milliseconds) you might need to switch to an incremental or even a concurrent GC to avoid the negative effect that long suspensions have on highly concurrent applications.
- Massively concurrent applications can be burdened by the fact that allocations need to be synchronized within the JVM. One can turn on thread-local allocation to remedy this (see the Making Garbage Collection Faster section earlier in this chapter). Thread-local allocation should not be confused with thread-local variables; the former is a JVM option to improve allocation concurrency. While this can help a lot, reducing the number of allocations will improve things even more. To achieve this we need to do an allocation analysis (see the Analyzing Performance Impact section earlier in this chapter).
When your application has a more variable load pattern, it’s quite difficult to achieve proper young generation sizing. There are a couple of work arounds:
- Use adaptive sizing and let the JVM size the young and old generation. In many cases, this will take care of the matter
- The Oracle Hotspot JVM allows you to test the Garbage First (G1) GC, a tool specifically designed for applications with a variable load pattern. Instead of having one young and old generation the G1 splits the heap in many smaller areas and dynamically assigns those spaces with the duty of being an young or old generation. This way it can more easily adapt to a changing load behaviour.
Frequent Major Garbage Collections
Suspending your JVM for a Major GC simply takes longer than for any other GC cycle. Therefore, frequent major GCs can quickly become a big performance problem.
Causes and Solutions
Most applications experience memory constraints from improper young-generation sizing, which results in premature object tenuring (See the Tuning section earlier in this chapter).
General memory constraints are just as often at the root of this problem. Increasing the total memory allocation for the JVM is the usual remedy. However, if the problem persists and you’ve already assigned several gigabytes of memory, the cause is more likely a memory leak, which can lead to an ever-growing old generation and, eventually, an out-of-memory situation.
Incorrect or non-optimized garbage-collection configurations are the most common cause for GC-related performance problems. These are also the easiest to fix, since they do not require any code changes. Memory leaks, as you will see in the next chapter, are a very different story.