Syntactic sugar (like the introduction of
var) is often not enough to convince somebody (especially non-developer) to move forward. This is one of the reasons why Java 8, almost 7 years after its first release, is still widely used. However, many things have improved apart from the syntax. When taking all of them into account, it may turn out that you can’t afford to run Java 8 anymore. And no, I don’t mean migrating the code, but simply updating the JVM used as a runtime!
Let’s outline few non-syntactic benefits of upgrading the JVM. For those still running on Java 8, they may become a good starting point for discussions about the change. To those already migrated, they may introduce some less obvious features of the recent JVM.
Note: In this article, I use the terms ‘Java’ and ‘JVM’ with regards to the Hotspot VM
Even the simplest Hello world! application needs some time to start (elapsed between calling
java -jar ... and printing the message). Recent comparisons show, that since Java 8 the application startup time has already almost halved (comparing to the early builds of Java 16), consistently progressing towards better performance. Although this part takes only a fraction of a second (or at least it should), when combined with numerous optimizations introduced on the JVM level itself results in a noticeable speedup. In other words, the same app running on the latest JVM should start faster than when running on Java 8.
In order to verify this, I took a real-life, moderate-sized Spring Boot 2.3 application written in Kotlin (with
jvmTarget = '1.8' to target Java 8 bytecode) and run it locally with Java 8, 11, and 15. As a startup time measurement, I’ve picked the value reported in logs by the Spring Boot itself. Although it’s not the best measure of the actual startup time (in this case Quarkus’ approach seems to be better), it should be good enough for comparison when the only thing changing is the JVM runtime.
As you can see, switching from Java 8 to 11 resulted in a 15% speedup (1 second faster). Java 15 turned out to be a bit slower than 11 (about ~200ms) for some reasons, but it still provides at least 12% improvement over Java 8.
Why it matters? In distributed (replicated) environments, faster startup means that other nodes have to survive the higher load (caused by the temporary decreased number of replicas) for a shorter period of time. This reduces the possibility of unexpected service downtime or even cascading failure of the whole system. Additionally, faster startups allow performing the system upgrades quicker and with higher reliability. That means they simply reduce the outage possibility.
Better garbage collection
Modern Hotspot JVM has at least 4 types of garbage collectors (GC) intended for production usage:
- Serial which uses a single thread to perform all the work
- Parallel (a.k.a. throughput collector) that uses multiple threads
- G1 (Garbage-First), a mostly concurrent collector
- low-latency collectors like ZGC or Shenandoah.
Note: there is also a no-op GC available called Epsilon, but this one is intended for various testing purposes.
Starting from Java 9, the default garbage collector has changed from Parallel to G1. The detailed comparison of both GC is far beyond the scope of this post, but in short, G1 blocks application threads less frequently (so-called “stop-the-world” phase), resulting in shorter GC pauses. In general, when preferring response time over throughput, G1 should be a better fit.
Why one should care about the GC pauses? One thing is obviously better application responsiveness. Pauses taking second or more (not so rare with Parallel GC under high load) may not only negatively impact user experience, but also cause failures of the dependent services (e.g. by exhausting their connection pool) or unexpected application restarts (when due to GC pause app would be identified as non-responsive one).
Same tools, but on steroids
One may say:
Wait, G1 is not anything new! It’s there since Java 7!
That’s definitely true, but G1’s stability & performance is improving with every new release. Only some of the changes (important bugfixes) are still being backported - simply, Java 8 does not provide the best possible experience in terms of G1 and it never will.
March 1, 2021(/me fixes another 8u G1 crash, marveling at old codebase in morbid fascination) If you are using G1, please consider upgrading past JDK 8. There was a lot of work in JDK 9 and beyond to stabilize G1. You really want most of that work.March 1, 2021
The same applies to almost every change or improvement being backported to Java 8 (like initial container awareness introduced with u191): even if significant, it won’t provide the same level of experience as newer JVMs will.
Automated GC selection
Interestingly, recent JVM releases come with another useful feature - the ability to automatically switch between Serial and G1 GC based on resource constraints (CPU, memory). The idea is based on the characteristics of both collectors - when operating in an environment with limited resources (e.g. single CPU or low memory environment) Serial, single-threaded GC is often a better choice than any of the multi-threaded ones.
This seems to be a bit under-documented Hotspot feature, as I did not find any specific criteria for such a selection. However, based on my experiments with Java 11 it looks like, that:
- with a single CPU, Serial collector is being selected regardless of the memory constraints,
- with more than one CPU, Serial collector is being selected when the environment has less than 1792 MB of available memory.
When running on Java 8, Parallel GC would be used by default regardless of the environment resource constraints. This could become especially painful when multi-threading capabilities are limited (e.g. single CPU). In such a case, starting multiple GC threads would only cause additional context-switching effectively slowing down the processing.
Proper GC selection is a huge topic of its own, so let’s conclude that recent JVM versions provide us more suitable defaults than before.
Upgrading from Java 8 does not always have to start from changes in the code. Java is far more than just a language, and the JVM experiences major changes and improvements with each new release. Switching just a runtime to the newer version should result in reduced startup time, better garbage collection & higher performance. All those features should also bring benefits in terms of reliability. Although few of the recent features have been backported to Java 8, they can’t compete with the level of experience provided by the more recent ones. Migrating away from Java 8 is not a matter of ‘if’ anymore, but only about the ‘when’.