All You Need To Know About Garbage Collection in Java
This post discusses memory management in Java, Garbage Collection, choosing the right collector, environment setup, and GC logs.
Join the DZone community and get the full member experience.Join For Free
Memory Management in Java
Automatic memory management is the key feature of Java. Memory management is the process of allocating new objects and deallocating or removing the unreferenced objects after their use to create room for new object allocation.
In traditional programming languages such as the C/C++ application, programmers have to manually perform memory management in the application with the creation and deletion of the objects after their use. There are high chances of a memory leak if the developer missed deleting the objects after usage. If there is more memory leak in the application, memory consumption keeps growing. At a certain point, the application might not get free memory for new object allocation, which will result in application failure with OutOfMemory Errors. It is for that very reason that automatic memory management in Java plays a key role in application performance.
In Java, while writing a program or application code, developers do not have to worry about memory management, as it is provided by the Garbage Collection feature in Java. Basically, Garbage Collection helps in identifying (marking) the unused objects in the heap memory section then it removes (sweeps) all the unreferenced objects from the heap memory. Post that it compacts the memory so that new object allocation will be smoother.
There are three steps involved in Garbage Collection:
In this stage, GC (Garbage collector) will scan the heap memory segment and mark all of the live objects which are having references by the application. All of the objects which are not having any references are eligible for garbage collection.
In the sweep stage, GC will recycle all the unreferenced objects from the heap memory.
After sweep, there are many regions in the heap memory that become empty which causes fragmentation. The compact phase helps in arranging the objects in the contiguous blocks at the start of the heap. This will help in allocating the new objects in sequence.
Heap Memory and Its Partition
Java objects created by the application reside in the memory called heap memory. Heap is created when JVM starts, and as per application usability, heap usage increases and gets full. When heap usage becomes full and further request for new object allocation doesn't have any free space in the current heap memory, then garbage collection happens. In the Garbage Collection above, three steps such as Mark, Sweep, and Compaction happen, which removes all the unreferenced objects from the heap memory and creates room for new object allocation.
Before understanding the heap memory segment we need to understand the concept of Generational Garbage Collection in Java.
What Is Generational Garbage Collection in Java?
In GC, marking, sweeping, and compacting are performed for all unreferenced objects from heap memory. As more and more objects allocate, the JVM heap is piled up with a large number of object allocations, which creates a longer time need for garbage collection. However, empirical or hypothetical analysis of applications has shown that most objects are short-lived. Therefore, marking and compacting all the objects from JVM heap memory is inefficient and time-consuming. For this reason, GC implements a generational garbage collection that categorizes objects based on their age (lifespan). With this process, objects are allocated in different generations and are garbage collected accordingly.
Heap memory is divided into majorly 2 areas:
1. Young generation(Nursery space)
2. Old generation
1. Young Generation (Nursery Space)
Whenever new objects are created they are allocated to the young generation. The young generation basically consist of two partitions.
A) Eden Space
All of the new objects are first allocated in the Eden space.
B) Survivor Space
After one GC cycle, all live objects from Eden space are moved to Survivor space. Survivor space is further divided into two parts, s1 & s2 space, also called FromSpace and ToSpace. Both Survivor spaces always start empty when JVM starts.
Below is the normal object allocation flow that happens in the young generation:
- At first, all the new objects are allocated into Eden space while both the survivor spaces are empty.
- When Eden space has filled there is no further space for new object allocation, it causes allocation failure and minor GC happens. In this stage of GC, all live objects are marked and moved to S1 space and Eden space is cleared while S2 is still empty.
- Next, when another minor GC happens, it will mark all the live objects from Eden & S1 space. With GC it clears all the live objects from Eden and S1 Space and moves them into S2 space. With this, Eden and S1 space will be empty. At any point in time, one of the survivors is always empty.
- In the next minor GC, the same process happens for Eden and S2 space after GC all the live objects are moved into S1.
2. Old Generation
When objects are long-lived in the young generation with multiple GC cycles, they are marked live in the survivor space. They will be eligible for promotion to old generations after completing the threshold of the GC cycle. These long-lived objects are further moved into the old generation. The old generation is also known as the tenured generation. The garbage collection events in this area are called major collections.
Full GC performs cleaning of all the generations(young + old generations). It performs promotion of all the live objects from the young generation to old generation, as well as compaction of the old generation. Full GC is the stop-the-world pause which will ensure no new objects are allocated and objects do not suddenly become unreachable while GC performs.
How Much Memory Does a JVM Consume?
Please note that the JVM uses more memory than just the heap. For example, Java methods, thread stacks, and native handles are allocated in memory separate from the heap, as well as JVM internal data structures.
The value of the -Xmx parameter controls the maximum size of the Java heap which is not the only memory that the JVM allocates. In addition to that the Permanent Generation or Metaspace(based on Java, from JDK 8 onwards Metaspace is there), CodeCache, native C++ heap used by the other JVM internals, space for the thread stacks, direct byte buffers, GC overhead, and other things which are included in JVM memory consumption.
The memory used by a JVM process can be calculated as follows:
JvmProcessMemory = JVMHeap + Metaspace + CodeCache + (ThreadStackSize* Number-of-Threads) + DirectByteBuffers + Jvm-native-c++-heap
A few of the other JVM memory components are below:
HotSpot JVM prior to JDK 8 had a third type of generation called Permanent Generation, which was contiguous with Java heap. It contains Metadata by the JVM to describe classes and methods used in the application. From JDK 8 onwards, Permanent has been replaced by a new space called Metaspace, which is not contiguous with the Java heap. Metaspace is allocated out of native memory. MaxMetaspaceSize parameter limits the usage of Metaspace in the JVM. By default, there is no limit for Metaspace, which starts with a very low size default and grows as needed in smaller increments. Metaspace only contains class metadata, and all live Java object parts are moved to heap memory so metaspace usage is much lower than Permanent Generation. Usually, there is no need to specify the maximum metaspace size unless you face a large metaspace leak.
Codecache is a memory area that contains native code generated by JVM. The JVM generates native code for a number of reasons. Such reasons include a dynamically generated interpreter loop, JNI stubs, and Java methods that are compiled into native code by the JIT compiler. JIT compiler contributes to most of the code cache area.
-XX:ThreadStackSize=size sets the thread stack size (in bytes). Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, or g or G to indicate gigabytes. The default value for Xss depends on the operating system and architecture you are running on. This option is equivalent to -Xss.
One can check the current thread stack size by the following command:
~~ jinfo -flag ThreadStackSize JAVA_PID ~~
Why Is Garbage Collection Important in Application Performance?
Garbage collection plays a key role in application performance, as it impacts unpredictability due to improper tuning. If there are frequent GC events happening which result in the Garbage Collector keeping busy in performing a GC operation and causing high CPU usage on the application server, it results in poor application processing.
If your garbage collections are happening too often or contributing to a significant percentage of your CPU, you should either increase the Java heap size settings or look for places in your application that are allocating memory unnecessarily.
Excessive garbage collection can happen either due to insufficient heap memory configuration or if there is any memory leak in the application, which further needs to be checked by generating heapdump during issue time.
For better system performance, it's necessary to have very few Full GC events, GC pauses should be at a minimum, and the percentage of CPU spent on garbage collection should be very low.
It is recommended that you test your applications under load in a development environment to determine the maximum heap memory usage. Your production heap size should be at least 25%-30% higher than the tested maximum to allow room for overhead.
Common GC Configurations and Parameters
-Xms: Sets the minimum and initial size (in bytes) of the heap
-Xmx: Specifies the maximum size (in bytes) of the heap
-Xmn: Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery) in the generational collectors
-XX: PermSize: Sets the space (in bytes) allocated to the permanent generation that triggers a garbage collection if it's exceeded. This option was deprecated in JDK 8 and superseded by the -XX: MetaspaceSize option.
For server deployments, -Xms and -Xmx are often set to the same value so that the heap size is fixed and pre-allocated.
Refer to java-commands for more information about different JVM options & their usage.
Choosing the Right Garbage Collector for My Application
The garbage collector's choice is based on different requirements. Based on throughput, latency, and footprint measurement, one can choose the GC collector which is best suited for their application.
Throughput is the percentage of total time not spent in garbage collection considered over long periods of time. Throughput includes time spent in allocation (however, tuning for speed of allocation generally isn't needed). For example, if your throughput is 95%, that means 95% of the time the application was running and 5% of the time the garbage collection was running. For any high-load business application, everyone wants high throughput.
Latency is the responsiveness of an application. Garbage collection pauses affect the responsiveness of applications. Therefore, latency should be as low as possible for better application performance.
Footprint is the working set of a process, measured in pages and cache lines. On systems with limited physical memory or many processes, footprint may dictate scalability.
Therefore, choosing the right collector is totally dependent on the application requirement. The application's object creation needs to be chosen wisely with proper testing in a lower environment with an expected production load.
Common Garbage Collectors
1. Serial Collector
The serial collector uses a single thread to perform all garbage collection work, which makes it relatively efficient because there is no communication overhead between threads.
It is best-suited to single processor machines because it can't take advantage of multiprocessor hardware, although it can be useful on multiprocessors for applications with small data sets. The serial collector is selected by default on certain hardware and operating system configurations or can be explicitly enabled with the option -XX:+UseSerialGC.
Serial GC would be the best choice for applications that do not have low pause requirements and work on very small heap sizes.
2. Parallel Collector
The parallel collector is also known as a throughput collector, and is a generational collector similar to the serial collector. The main difference between the serial and parallel collectors is that the parallel collector has multiple threads that are used to speed up garbage collection.
The parallel collector is intended for applications with medium-sized to large-sized data sets that are run on multiprocessor or multithreaded hardware. You can enable it by using the -XX:+UseParallelGC option.
Parallel compaction is a feature that enables the parallel collector to perform major collections in parallel. Without parallel compaction, major collections are performed using a single thread, which can significantly limit scalability. Parallel compaction is enabled by default if the option -XX:+UseParallelGC has been specified. You can disable it by using the -XX:-UseParallelOldGC option.
Parallel Collector would be the best choice where throughput is more important than latency. One can use Parallel Collector where long pauses are acceptable, such as bulk data processing or batch jobs.
3. The Mostly Concurrent Collectors
Concurrent Mark Sweep (CMS) collector and Garbage-First (G1) garbage collector are the two mostly concurrent collectors. These collectors perform some expensive work concurrently to the application so they are named mostly concurrent collectors.
CMS collector is preferred in the application environment where low garbage collection pauses are required and can share the processor resources with the garbage collector while the application is running. This collector basically gives more benefits where long-lived tenured generation is high in the application or when running on a machine where two or more processors are available. The CMS collector can be enabled with the option -XX:+UseConcMarkSweepGC.
CMS collects the tenured generations by performing garbage collections concurrently with the application thread. It ensures low pauses in the application. In a normal operation, CMS collector does marking and sweeping concurrently with the application threads, which results in shorter pauses. However, if the CMS collector is unable to clear the unreferenced objects before the old generation fills up or if the allocation can not be satisfied with available space in the old generation then CMS will stop all the application threads and perform the garbage collection. The inability to complete the garbage collection concurrently is known as concurrent mode failure and indicates the need to adjust the CMS collector parameters.
If more than 98% of the total time is spent in garbage collection and less than 2% of the heap is recovered, then an OutOfMemoryError is thrown by the CMS collector.
This feature is designed to prevent applications from running for an extended period of time while making little or no progress because the heap is too small. If necessary, this feature can be disabled by adding the option -XX:-UseGCOverheadLimit to the command line.
The CMS collector is deprecated as of JDK 9. It's recommended to use the Garbage-First collector instead.
Why has the CMS collector been deprecated?
According to JEP-291, CMS collector has been deprecated from JDK 9. To accelerate the development of other garbage collectors in HotSpot, CMS collector was deprecated. It will reduce the maintenance burden of the GC code base and accelerate new development. JDK 9 onwards deprecates CMS so that a warning message is issued when it is requested on the command line, via the -XX:+UseConcMarkSweepGC option.
Trying to use CMS via the -XX:+UseConcMarkSweepGC option will result in the following warning message:
Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; \
support was removed in <version>
What's the alternate choice for CMS collector now?
- The G1 garbage collector is intended, in the long term, to be a replacement for most uses of CMS.
- New collectors, ZGC and Shenandoah can be used with the latest JDK for the replacement of CMS.
- If one of the above collectors doesn't work for your application requirement, users can still use CMS as long as it remains supported in earlier releases.
G1 Garbage Collector
G1 is a server-style collector for multiprocessor machines with a large amount of memory. It meets garbage collection pause-time goals with high probability while achieving high throughput with little need of configurations. G1 is selected by default on certain hardware and operating system configurations or can be explicitly enabled using -XX:+UseG1GC.
G1 can be used to achieve high throughput and low latency for the applications which falls under the below categories:
-Larger heap size, specifically more than 6+ GB's & with more than 50% of the heap occupation is live objects
- G1 could be the best to use when allocation rates and promotion can vary significantly over time.
- A large amount of fragmentation in the heap
- To achieve a predictable amount of short pauses not longer than a few hundred milliseconds
G1 replaces the Concurrent Mark-Sweep (CMS) collector, and it is also the default collector. You can explicitly enable it by providing -XX:+UseG1GC on the command line.
G1 is a generational, incremental, parallel, mostly concurrent, stop-the-world, and evacuating garbage collector which monitors pause-time goals in each of the stop-the-world pauses. Similar to other collectors, G1 splits the heap into (virtual) young and old generations. Space-reclamation efforts concentrate on the young generation where it is most efficient to do so, with occasional space-reclamation in the old generation.
G1 reclaims space mostly by using evacuation: live objects found within selected memory areas to collect are copied into new memory areas, compacting them in the process. After an evacuation has been completed, space previously occupied by live objects is reused for allocation by the application.
The Z Garbage Collector
The Z Garbage Collector (ZGC) is a scalable low latency garbage collector. ZGC performs all expensive work concurrently, without stopping the execution of application threads for more than 10ms, which makes it suitable for applications that require low latency and/or use a very large heap.
The Z Garbage Collector is available as an experimental feature and is enabled with the command-line options -XX:+UnlockExperimentalVMOptions -XX:+UseZGC.
Setting up a max heap size to ZGC is a very important aspect as it depends on allocation rate variance & how much live set of data is in the application. In general, giving more heap to ZGC is always better; however, wasting memory is also not efficient. So it's all about finding a balance between memory usage and how often GC needs to be run.
Setting XX:ConcGCThreads Number of Concurrent GC Threads is also an important tuning part in ZGC as this parameter specifies how much CPU-time the GC should be given. So giving too much of CPU-time GC will consume more CPU from the application, and giving too little will cause garbage to be generated more compared to garbage collected by GC.
Shenandoah Garbage Collector
Shenandoah is another type of low pause time garbage collector that reduces GC pause times by performing more garbage collection work concurrently with the running Java program. Shenandoah does the bulk of GC work concurrently, including the concurrent compaction, which means its pause times are no longer directly proportional to the size of the heap. Garbage collecting a 200 GB heap or a 2 GB heap should have a similar low pause behavior.
Shenandoah is the best to suit an application that needs responsiveness and short pauses, regardless of heap size requirements. You can configure -XX:+UseShenandoahGC JVM option to enable Shenandoah GC.
How To Analyze the GC Logs
There are multiple tools available for analyzing the gc.log, which include IBM Pattern Modeling and Analysis Tool for Java Garbage Collector, GCviewer tool, and garbagecat. Even one can analyze the gc.log by reading the gc.log in the txt file also.
I am sharing one of the garbagecat tool details as it’s quite easy to use and shows great results by analyzing the gc.log within a few minutes.
This is a command-line tool that parses Java garbage collection logging and does analysis to support JVM tuning and troubleshooting for OpenJDK and Sun/Oracle JDK. It differs from other tools in that it goes beyond the simple math of calculating statistics such as maximum pause time and throughput. It analyzes collectors, triggers, JVM version, JVM options, and OS information and reports error/warn/info level analysis and recommendations.
- Sun/Oracle JDK 1.5 and higher
How Can I Use garbagecat?
After installing garbagecat, you can use a simple command for getting a report for your GC logs:
~~ java -jar garbagecat.jar -p /path/to/gc.log ~~
The report contains six sections: (1) Version, (2) Bottlenecks, (3) JVM, (4) Summary, (5) Analysis, and (6) Unidentified log lines.
A bottleneck is when the throughput between two consecutive blocking GC events is less than the specified throughput threshold.
An ellipsis (…) between log lines in the bottleneck section indicates time periods when throughput was above the threshold.
If the bottleneck section is missing, then no bottlenecks were found for the given threshold.
You can get a good idea of where hotspots are by generating the report multiple times with varying throughput threshold levels.
Sample garbagecat Report Output for Reference:
Opinions expressed by DZone contributors are their own.