JVM – Garbage Collection
WARNING! – A lot of this content is just regurgitated content from the java magazine article that’s referenced below, if you’d like more detail, please refer to the article, this is a shortened overview at most. I’ve written this article to help educate myself & hopefully others! 🙂
Having read an article from Java magazine, it is reasonable to state that older garbage collection solutions such as parallel garbage collector have a higher throughput. Prior to Java 9, the parallel GC was the default garbage collection solution.
Increasing the heap size of an application will increase the throughput, regardless of the GC that you’ve decided to use. However, since the heap is bigger, this can make the pause times longer since there’s essentially more work to be processed by the garbage collection, per cycle. But on another note, there will be less & less garbage collection cycles since the heap size is increased, allowing more memory to be allocated.
This means that in versions of Java older than 9, using parallel GC on a large heap can cause significant pause times, because of the time it takes to collect the old generation of allocated objects.
Since Java 9, the default garbage collector is the G1 collector for the OpenJDK & the Oracle JDK, the general function of the G1 solution is to slice up the garbage collection pauses according to a user-supplied time target. An example of this would be using VM arguments such as -XX:MaxGCPauseMillis=500. This will essentially give you more control over the pause time regarding the garbage collection cycle, although the G1 garbage collector may offer lower pause times, it does in fact tend to have a higher throughput.
Although, the G1 garbage collector is far from perfect, as the amount of work to be processed during a garbage collection cycle increases, either due to a large heap or allocating a lot of objects, the time slicing approach gets bogged down. As the article states, a good analogy would be cutting large pieces of food into smaller pieces, sure they’re easier to digest, but if you have an obscene amount of food on your plate, it’ll take you a lifetime.
New Garbage Collection Solutions
Since JDK12, there have been two new garbage collection solutions, one being the Shenandoah garbage collector, the other being the ZGC garbage collector. Below I’ll go into a bit more detail as to why they’ve come into existence, what’s so great about them & so on.
There are two new garbage collection solutions that have come into existence with Java 12, one being the Shenandoah solution, the other being the ZGC solution. Both have very similar goals, but they typically tend to approach the problems in different ways.
The Shenandoah solution is an advance on the G1 solution, it tries to of more of its garbage collection cycle work concurrently. This means that it can move heap regions, in English, it can relocate objects concurrently within the application, to achieve the concurrent relocation, it uses what’s known as a Brooks Pointer.
To simplify, this pointer is an additional field in each object in the Shenandoah heap, which points back to the object itself. The purpose behind this is merely since when an object moves from one region to another, it also needs to update other objects in the heap that have references to this object in question. So, it leaves the Brooks pointer in place, allowing the solution to essentially forward all references to the new location of the object.
If for whatever reason you can’t make the jump to Java 12, it may be of interest looking into the backports to Java 8 & Java 11.
The primary goals of ZGC are low latency & ease of use, to achieve this, ZGC will conduct garbage collection while the garbage collection operations take place. The only time this rule doesn’t apply is when the thread stack scan takes place.
One incredibly impressive feature of the ZGC is how it can scale from only a few hundred MB through to TB sized Java heaps. Even with larger heap sizes, the pause time is kept to a minimum, research states that the worst-average case scenario is 10ms, although the average appears to be 2ms.
The ZGC solution was in fact added in Java 11 as an experimental feature, however, in Java 12, ZGC included the support for concurrent class unloading, allowing Java to continue running during the unloading of unused classes, instead of causing some form of pause to take place. To achieve concurrent class unloading can be complicated, hence why previously this process has not been concurrent & traditionally has been done as a stop-the-world event.
In this document, I will not cover how ZGC runs concurrent class unloading, if you’d like to know more, then please refer to the link provided to the Java magazine article. But I will cover areas around this topic, such as how once the reference processing has taken place, ZGC will know which classes are no longer needed.
The next step from having found all the unneeded classes is to clean all data structures, which contain stale & invalid data, as a result of these dying classes. The links between existing & valid data structures & invalid or dead data structures are cleared. The unlinking process includes a few internal JVM data structures, including the code cache, which contains all the JIT-compiled code, the class loader data graph, the string table, & so on.
Once these dead objects have been un-linked, they are walked again to delete them to ensure that the memory is reclaimed. Until now, all JDK garbage collection solutions have done all of this as a stop-the-world event, causing latency issues for Java applications, for applications that require a low latency, if not real time, this can & has been an issue.