What is Java Garbage Collection?
Most Java developers know that there is something called Garbage Collector in JVM, which automatically reclaims the objects from the memory that they are creating. They also think they don’t have to worry about it, as Garbage Collection is automatic and JVM takes care of it. It’s a true statement. However, there are going to be cases where a Java developer needs to ‘learn a little bit’ more about Garbage Collection, especially when their application suffers from memory problems or performance degradations. This post intends to provide a detailed overview of Java Garbage Collection and this often ignores great benefits.
What is Garbage?
Java application creates several objects to service the new incoming requests. For instance, when a customer signs on, our applications creates following objects: an HTTP request object, HTTP session object, HTTP filter object, Servlet object, customer object (which internally includes username and password string objects), a DAO object for backend database interaction, a connection object for database connection, and several such objects.
Once the request is serviced, these newly created objects will become useless. Because these objects are specific to a particular customer sign on request, and it can’t be re-used. Now these objects are called ‘Garbage’.
This garbage has to be removed from memory. Because your memory is a finite space, it needs to free up to service the other new incoming requests. The process of removing such unreferenced objects from memory is called ‘Garbage Collection’.
Garbage Collection Evolution
In earlier days, especially in languages like C and C++, to service the new incoming requests, developers had to manually allocate and as well deallocate memory using ‘malloc()’ and ‘free()’ APIs, respectively. Once the request is serviced, they need to write code to deallocate those objects from memory. Business applications are complex, there are multiple workflows and use cases. If a developer misses to deallocate objects in just one workflow, then those objects will start to build up in the memory, eventually causing OutOfMemoryError. Thus, OutOfMemoryError was quite pervasive in those days.
When Java was introduced in 1995, it promised automatic garbage collection. While some languages before Java also had automatic garbage collection, Java really championed it. Java developers only need to allocate objects; and deallocation is handled by the Java runtime environment. Developers love this feature because it reduces memory leaks and allows them to focus on business logic. From that point on, all modern programming languages come with this automatic garbage collection capability.
Side-effects of Automatic GC
So far, this automatic garbage collection sounds good right? It’s good, but it comes with two side effects:
a. Response Time degradation: To do automatic garbage collection, Java Runtime Environment has to keep track of all the objects created in memory. It should identify what are the active objects and inactive (i.e. unreferenced) objects and sweep them out of memory. To do this, the Garbage Collector pauses your application. This pause can span from a few milliseconds to several seconds (sometimes even minutes). During the pause window, your customer transactions are not processed. Thus, your application’s response time will degrade.
b. High CPU consumption: Garbage Collection is a highly CPU-intensive operation. Modern applications create a large number of objects. All those objects must be constantly tracked. Their incoming references, outgoing references need to be constantly checked. Unreferenced objects need to be identified and removed from memory. As a result, automatic garbage collection consumes enormous amounts of CPU cycles. If your application consumes 70% CPU, it’s likely that more than 40% of CPU cycles are consumed by Garbage Collection.
Garbage Collection – Life partner of your application: GC is like a life partner of your application. Seriously , there are a lot of similarities: 1. The GC coexists with your application under the same roof (i.e., the JVM). 2. GC keeps cleaning up the mess (i.e., garbage) created by your application. If the application gets aggressive and creates a lot of objects, GC will also become aggressive to free up the space in the memory. 3. Both GC and your application continuously fight for the same CPU cycles. Just like how choosing the right life partner is essential for the happiness of your life, choosing the right Garbage Collector and GC settings is essential for a high-performing application.
JVM Memory Regions
Fig: JVM Memory Regions
To minimize these overheads, Java Garbage Collector adopted a clever strategy of breaking memory into smaller internal regions, so that the number of objects that JVM needs to scan will be limited. Here are the different JVM memory regions:
1. Young Gen
2. Old Gen
3. Metaspace
4. Others
Whenever a new object is instantiated, it will be created in the Young Gen of the JVM’s Memory. All newly created objects are stored in the Young Gen. For example, if we create Servlet, DAO, Pojos… objects to service a new transaction, once a transaction is serviced, these objects will no longer have any reference. It should be reclaimed from memory. However there are certain objects which live for a long duration, like: DB connection, Cache, Session… JVM promotes objects that live for a longer duration from the Young Gen to the Old Gen. Metaspace contains metadata definitions (i.e., class definitions ‘Customer.class’ and method definitions) that are required to execute the program. Other regions contain internal regions like GC region, Threads Region, Internals, Code Cache… – which you don’t have to go into detail, unless you are deep into JVM troubleshooting and optimization. However, if you are interested, you can learn more about JVM internal memory regions from this video clip.
What is the typical size of these Memory Regions? It varies from application to application. However, for most applications, we can assume that Young Gen will occupy 30% of memory, Old Gen will occupy 60% of memory, Metaspace & Others Region will occupy the remaining 10%.
Types of GC Events
From a high level, there are two types of Garbage Collection Events:
a. Young GC (aka Minor GC)
b. Full GC (aka Major GC)
The Young GC event runs only on Young Gen. It scans and reclaims unreferenced objects from the Young Generation.
On the other hand, Full GC event scans and reclaims unreferenced objects from all the memory regions (Young Gen, Old Gen and Metaspace).
Young GC takes less time to complete, because it has a smaller region to scan & reclaim, whereas Full GC comparatively takes longer time to complete because it must scan & reclaim from large regions. Several JVM studies have indicated that 80% of objects are short lived. Thus, JVM triggers Young GC more frequently than Full GC. This strategy of breaking down memory into smaller internal regions, eliminates the need to scan the entire memory region and minimizes overhead incurred by the Garbage Collection.
Does Young GC pause the Application? There is a wrong education going on in the industry that Young GC don’t pause the JVM and only Full GCs do pause the JVM. It’s not true. Both Young GC and Full GC do pause the JVM. Since Young GC is working only on a smaller region (i.e. Young Gen), it will comparatively take less time to complete than Full GC which needs to work on a larger region (i.e. Young Gen, Old Gen, Metaspace and Others regions).
Garbage Collection Algorithms
At the time of writing this post, there are 7 GC algorithms in OpenJDK. Let’s review those algorithms quickly here:
1. Serial GC
The Serial Garbage Collector is the first GC algorithm introduced in JDK. Serial GC performs all garbage collection work on a single thread. This means it stops all application threads (stop-the-world) during garbage collection, which can lead to noticeable long pauses in the application. If you are interested, learn more about Serial GC tuning here.
Note: Since pause times are high in Serial GC, it’s not ideal to use Serial GC in our modern-day applications.
2. Parallel GC
Unlike Serial GC, Parallel GC utilizes multiple threads to perform garbage collection phases. This allows garbage collection work to be distributed across multiple CPU cores, reducing the impact of garbage collection pauses on application performance. If you are interested, learn more about Parallel GC tuning.
Note: If your focus is to achieve better GC throughput over occasional high GC pause times, you can use Parallel GC. Parallel GC is well suited for batch applications and business applications in which consistent GC pauses at regular intervals are acceptable.
Recommended by LinkedIn
3. Concurrent Mark Sweep (CMS GC)
The Concurrent Mark-Sweep (CMS) Garbage Collector is designed to minimize the application pause times by performing most of its garbage collection work concurrently with the application threads. The primary downside of CMS is that it does not compact the heap, which will lead to fragmentation, eventually causing long GC pauses. If you are interested, learn more about CMS tuning.
Note: CMS algorithm has been deprecated in Java 9 and removed completely from Java 14. Since this algorithm is deprecated and removed, I wouldn’t use the CMS algorithm for my applications, unless there is a specific need.
4. Garbage First (G1 GC)
The Garbage First (G1) Garbage Collector is an adaptive algorithm. G1 divides the heap into equal-sized regions, allowing for flexible and efficient memory management. It performs most operations concurrently, reducing stop-the-world pauses. G1 aims to provide the best balance between latency and throughput, as long as your heap size is not very large. If you are interested, learn more about G1 GC tuning.
Note: Since G1 GC has become the default algorithm since Java 9 and it’s consistently enhanced over the years, I will consider using this algorithm especially if my heap size is less than 32gb.
5. Shenandoah GC
The Shenandoah Garbage Collector is one of the latest and low-pause-time GC algorithms introduced by Red Hat for the OpenJDK. This algorithm is designed to achieve minimal pause times for applications with large heap size. Similar to G1, Shenandoah divides the heap into regions, allowing for targeted collection of memory regions with high garbage content, thus optimizing memory reclamation. If you are interested, learn more about Shenandoah GC.
Note: If your application has a large heap (say more than >32gb) and you can tolerate high CPU consumption, consider evaluating this algorithm.
6. Z Garbage Collector (ZGC)
The Z Garbage Collector (ZGC) is a scalable, low-latency garbage collection algorithm. Designed to manage very large heaps with minimal pause times, ZGC aims to keep GC pauses below 10 milliseconds, making it ideal for applications requiring consistent responsiveness and large memory allocations. Initial versions of ZGC had poor performance, but significant improvements have been made in recent versions. If you are interested, learn more about ZGC tuning here.
Note: If your application is running on a large heap and running on the latest version of Java (say > Java 21), you can consider evaluating this algorithm. However, you need to be aware of allocation stalls.
7. Epsilon GC
The Epsilon is basically a no-op GC algorithm, i.e. it will not reclaim any objects from the memory. Basically, when the heap gets full, the application will crash with an OutOfMemoryError.
Note: You can use Epsilon GC, in Performance labs to study the impact of GC pauses or for experimentation purposes but not in a production environment.
Below table summarizes each GC algorithm in nutshell
Choosing the Right Garbage Collector
You want to choose the garbage collection algorithm based on your GC tuning goal i.e. whether you want to keep low GC pause time, high GC throughput, reduce CPU consumption, … Here is a post which can assist you in choosing the right Garbage Collector algorithm for your application.
GC Tuning Benefits
Tuning Garbage Collection settings provides unbelievable benefits (which are often overlooked). Let me highlight few key benefits of GC Tuning:
Learn in detail about the hidden benefits of GC Tuning.
GC Analysis Tools
Best way to study and tune the GC performance is through GC Log analysis tools such as GCeasy, IBM GC Visualizer, HP JMeter, Garbage Cat. For example, tools like GCeasy identifies the GC and memory bottlenecks in your application and reports it as shown below:
Fig: GC problems reported by GCeasy tool
This tool also reports the appropriate recommendation to enhance your GC performance. Here is a live GC log analysis report generated by GCeasy. You can learn to do GC log analysis, by following the instructions given in this post.
GC Tuning Practical Tips
Here are nine quick tips to help reduce long garbage collection pauses and enhance your application’s performance. For deep insights into these tips, refer to the ‘9 Tips to Reduce Long Garbage Collection Pauses’ post.
GC Tuning Real World Case Studies
In this section, let’s review few interesting GC Tuning real world case studies, that resulted in tremendous performance improvements:
If you like, learn more about the fascinating real world GC tuning success studies.
Conclusion
Garbage Collection optimization brings several hidden benefits to your organization. I would like to strongly encourage you to learn this lifetime skill and become a superhero in your organization.