Freepik

Key Reasons To Adopt Java 11

Doug Ross
Skills Matter

--

— Ben Evans

Ben Evans is co-founder of jClarity, a JVM performance optimization company, has authored a number of core Java-related books (‘The Well-Grounded Java Developer’ and the new editions of ‘Java in a Nutshell’, ‘Java: The Legend’ and ‘Optimizing Java’), is a leading speaker and educator in the Java community and was a voting member on Java’s governing body — the JCP Executive Committee — for 6 years. Check out his GitHub profile and find him on Twitter.

Despite Java 9 having been out for over a year now, to date, very few projects have migrated to that version (or any post-8 version). This was due to two main reasons:

1. No real perceived major benefits over existing Java 8
2. Uncertainty over the new Java release model

The second of these was made visible in a reluctance by teams to move to a version (e.g. Java 9 or Java 10) that lacked Long-Term Support.

Most application shops do not want to deal with needing to upgrade their JDK version every few months or so. This means that, in practice, the so-called LTS releases are the only ones that the majority of teams seem to want to deploy into production.

Even though (in practice) the 6-monthly feature releases of the JDK are much more like point releases than major versions, psychologically, developers and ops teams are wary of putting short-lived binaries into production. As a result, the Java community as a whole has not made a significant move away from Java 8 yet.

With the arrival of Java 11 (in September 2018) — is all of that about to change?

In this post, we look at a few reasons why you might want to take a look at Java 11 for your applications. If you’re keen to learn more about the advantages and pitfalls involved in migrating to Java 11, then you might want to attend my two-day “Migrating to Java 11” course for a full deep-dive and the opportunity to get hands-on with Java 11.

Let’s get started, and meet some good reasons for choosing Java 11 for your applications.

You’re about to start a greenfield microservices project

This one cuts right to the heart of the matter — Java 9 was all about delivering Java modules, aka JPMS, aka Project Jigsaw.

This key feature allows for the delivery of Java code in units larger than a single package, with well-encapsulated APIs and the ability to have truly private internals.

However, the simple truth is that migrating an existing Java 8 application to modules is not a straightforward task.

It requires an honest assessment of how much re-engineering and refactoring work is required. For this reason, many teams may be reluctant to rearchitect an existing app — the “if it ain’t broke, don’t fix it” principle comes into play here.

A brand new greenfield project is another story completely.

These types of projects present a golden opportunity to do things properly, and if the project is based around microservices (as so many are these days), then so much the better. A new beginning means that the team can build using a modular approach right from the start.

Adopting a modules-first, modules-always design aligns well with microservices as the additional discipline in how the code needs to be structured helps the microservices stay small and helps developers think about the interface/implementation split.

The ability to start a radically smaller runtime (by only including the modules you need) provides several benefits — most obviously a smaller memory footprint and (potentially) much faster startup. For applications that deploy to Docker or other containers, then the Java 11 runtime is a much better fit than what was available in Java 8.

As HTTP/2 and its performance benefits roll out across the ecosystem, then Java 11’s native HTTP/2 support will be an advantage when interoperating with other services that rely upon the new version of the protocol.

Finally, Java 11 ships a very different (and much improved) version of G1 compared to the version that was present in Java 8. For microservices that care about consistency of GC pause times, this can be a significant advantage.

You have a big heap with lots of strings

Let’s turn from thinking about small microservice-based apps to huge beasts.

A relatively small change in Java 9’s implementation could pay off big, especially if you’re running a large heap. To understand the change and its impact, let’s recall some very basic Java:

A char is a primitive type that is 2 bytes (16 bits) in size. This is unlike C and C++ where a char is typically 1 byte (8 bits) in size.

So, what is the second byte in Java?

Well, this is something of a shaggy dog story about character sets, but here goes.

Java, right from way back in version 1.0, has supported Unicode — but it does so in a slightly complicated way.

First off, Java source code can be written in Unicode, encoded as UTF-8.

This means that you can write totally legal Java code like this (try it out in the JShell command-line tool, one of the other great new features in modern Java):

Function<String, Integer> λ = s -> s.length();
System.out.println(λ.apply(“^‿^”));

UTF-8 is a variable-width encoding, meaning that if you’re writing ASCII characters, it takes just 1 byte to encode them, but if you want to access other characters in Unicode space, it may take 2 or 3 or even 4 bytes to fully encode them.

So far, so good, but that’s just Java source code.

At runtime, when Java creates a new String in the heap, it has to represent the characters of the string. In Java 8 and below, that’s done by the String holding a reference to a char[].

The encoding used was originally UCS2, which means that every character is represented at runtime by 2 bytes — so this is a fixed-width encoding, unlike UTF-8.

However, the number of characters added to Unicode since then means that UCS2 was not sufficiently large to hold everything that is needed (e.g. emojis!). So in Java 5, UCS2 was replaced by UTF-16, which is also a variable-width encoding.

Fortunately for the Java programmer, this change was almost totally invisible to the end user, as it agrees with UCS2 for almost all of the 2-byte characters. The variable encoding only becomes an issue when handling certain supplementary characters (as Sun/Oracle refer to them).

OK, so putting this all together, almost all characters in Java strings are represented by 2 bytes at runtime — and that includes ASCIIish characters that actually only took up one byte in the source.

So what’s in the first byte of each char for ASCII strings?

A zero byte.

That’s right, for all of this time (over twenty years of Java), every ASCII string in every JVM on the planet has been using roughly twice as much memory as it needs to.

That is a lot of zero bytes.

So how does Java 11 help?

It provides a new representation that decides at runtime whether a string can be represented in Latin-1 (a single-byte representation that contains ASCII and Western European language characters) or not.

If it can, then the string is represented as a byte array (with the bytes understood to correspond to Latin-1 characters). This saves the empty zero bytes of the 2-byte char representation. If not, it’s business as usual, and the string is represented in UTF-16.

What’s great about this is that it’s a per-String opportunity to optimize — it’s not a per-heap choice.

In the source code of the String class, this code looks like this:

private final byte[] value;

/**
* The identifier of the encoding used to encode the bytes in
* {@code value}. The supported values in this implementation are
*
* LATIN1
* UTF16
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*/
private final byte coder;


static final byte LATIN1 = 0;
static final byte UTF16 = 1;

So, internally, instead of pointing at achararray, the String class now always has abytearray field, and the JVM unpacks the UTF-16 representation directly into bytes if it needs to.

The impact on applications that handle a lot of string data can be profound.
For example, on an ElasticSearch node with a 20G heap on Java 8 (which is mostly string data), that’s a lot of extra memory needed.

Just by moving to Java 11, that heap size could be cut almost in half, at a stroke.

This feature has other knock-on effects. For example, larger heaps have longer GC pause time. This means that an application architect could choose to reduce STW pause time by being able to shrink the heap size. Alternatively, the amount of sharding across a cluster could be reduced, as less space is being wasted on those pesky zero bytes, so more application data can be fitted into the same size heap.

You’re doing a lot of cryptographic calculations in your app

In Java 11, the support for cryptography has been improved in two major ways:

  1. Modern cipher suites and protocol versions
  2. Better intrinsic support for hardware acceleration of cryptography

The first point is fairly self-explanatory.

The suites and protocol have been updated, to include TLS v1.3 and associated supporting technologies (including RSASSA-PSS and enhancements to elliptic curve cryptography providers). This helps to future-proof Java and ensures that an application running Java 11 will be able to send and receive encrypted communications with a peer running even the latest and greatest software stacks.

The second point is a little deeper and more interesting.

Unlike ahead-of-time compilation (which is the proper name for what the C/C++/Go folks just call “compilation”), JIT compilation can decide on compilation strategies at runtime.

This sounds familiar enough, but one often-overlooked aspect of this is that the JIT compiler always knows exactly which CPU it is running on.

This means that the OpenJDK developers can ship an extensive ‘cookbook’ of optimizations which are highly processor-specific.

When we say “specific”, we don’t mean specific at the “Intel vs Sparc vs ARM” level, but at a much finer granularity — able to target individual processor generations and specific CPU products.

Not all the optimizations shipped will be able to run on every processor, but that doesn’t matter. Instead, when the JVM starts up, it probes the CPU in use and figures out exactly which optimizations are available on this particular chip. Then, when JIT compilation occurs, the code emitter knows exactly what optimizations from the cookbook can be used.

These processor-specific speedups are known as processor intrinsics, and some common examples include:

System.arraycopy() — this is one of the most common intrinsics and uses vector operations
System.currentTimeMillis() — this has a special OS-supported fast path on Linux
Compare-and-swap (CAS) — this is supported on almost all modern processors but was originally introduced as an intrinsic due to e.g. Sparc32 chips
Math.log10() — this can be done with 2 instructions on x86_64

Java 11 includes a good number of new intrinsics, which provide point speedups for some operations. Notably, among them are some cryptographic operations, including AES-specific instructions and enhancements for TLS. Applications that are doing a lot of cryptographically intensive work may well see an immediate benefit in terms of performance just by moving to a Java 11 runtime.

Of course, intrinsics are just one of the techniques that HotSpot’s JIT compiler uses to provide super-fast performance.

If you’re interested in learning more about them, and the rest of the suite of techniques used to ensure that your Java code runs as fast as possible, there’s a full treatment in my book Optimizing Java (O’Reilly, 2018), and I’m giving a 2-day class, entitled Deep Within The JVM’, where we’ll explore these ideas at some length.

There are many more reasons why Java 11 is attractive to development teams — especially for new projects — but hopefully, these three very different examples give some perspectives as to the range of applications that can benefit from the advances in this new version.

👍 For news and articles from Skills Matter, subscribe to our newsletter here.

--

--