Java 10 overview
March 2018 saw the latest semi-annual release of Java: Java 10.
In this article, we’ll examine the big changes introduced in this version, as well as talk about some of the smaller improvements that will make life easier for developers and ops alike.
Java 10: Big changes
The two big stories in Java 10 are:
- the new var keyword, just like you’d imagine with any new language construct, and
- the new six-month release cycle
Also, developers will be excited to see more API evolution.
And there are runtime improvements, new performance tuning knobs, and the now-perennial garbage collection improvements that we’ve come to expect with each release.
But there are a number of other interesting things, too, especially if you know how to read in between the lines and look ahead to Java 11 in September.
Local variable type inference
With the exception of assert from the Java 1.4 days, new keywords always seem to make a big splash, and var is no different.
Perhaps the most curious thing about it is that it isn’t actually a reserved word, but something else entirely. More on that in a moment.
What the var keyword does is turn local variable assignments:
HashMap<String, String> ughThisIsSoVerbose = new HashMap<>();
var succinct = new HashMap<String, String>();
Simply put, so long as the construct on the right-hand side doesn’t require a target type on the left-hand side (like lambdas do), then you can make all kinds of code easier to read:
var tshirts = Lists.of("Baeldung Medium", "Java Large", "Lua Small"); var lines = Files.get(Paths.get("log/catalina.out")); var length = lines.count();
In other words, Java 10 introduces local-variable type inference to the language. It figures out at compile-time the reference type based on the value type.
Now we can append this to the growing list of type inferences that Java makes, already including type inference with generics and with lambda expressions.
This feature has been a long time coming. It was suggested as far back as 2001, and was closed at that time with the following comment by Gilad Bracha:
Humans benefit from the redundancy of the type declaration in two ways. First, the redundant type serves as valuable documentation – readers do not have to search for the declaration of getMap() to find out what type it returns. Second, the redundancy allows the programmer to declare the intended type, and thereby benefit from a cross-check performed by the compiler.
Times have changed though, and the Java language is learning about the benefits of choice.
For example, there are situations where var‘s added succinctness may make the code harder to read:
var x = someFunction();
The above snippet is completely valid Java 10 code, and it’s absolutely confusing to read.
It’s confusing because it’s impossible for the reader to tell x‘s type without tracking down someFunction‘s return type. Similar complaints have been levied against dynamically-typed languages for years.
And, of course, this specific usage is precisely what Gilad warned the community about over 15 years ago.
So, use var with care, and remember that the goal is to write readable code.
And it’s actually not a reserved word
Don’t let folks tell you it is a reserved word. Under the hood, var is a special new type in Java.
So, actually, you can still use var in other places in your code, say as a variable or class name. This allows Java to remain backward compatible with pre-Java 10 code that may have made the (interesting) choice of naming a variable or two “var“.
And there is a lot more to this story! Read up on using var with non-denotable types as well as var‘s limitations surrounding polymorphism and lambda expressions in Oracle’s guide to local-variable type inference.
Unmodifiable collection enhancements
To introduce this next enhancement, consider the following Java puzzler. What is the value of v at the end of this program:
var vegetables = new ArrayList<>(Lists.of("Brocolli", "Celery", "Carrot")); var unmodifiable = Collections.unmodifiableList(vegetables); vegetables.set(0, "Radish"); var v = unmodifiable.get(0);
The answer, of course, is Radish. But, isn’t unmodifiable, well, unmodifiable?
Unmodifiable vs unmodifiable view
Actually, accordingly to Java 10’s updated Collection Javadoc, unmodifiableList returns an unmodifiable view collection:
An unmodifiable view collection is a collection that is unmodifiable and that is also a view onto a backing collection.
Examples of unmodifiable view collections are those returned by the Collections.unmodifiableCollection, Collections.unmodifiableList, and related methods.
Note that changes to the backing collection might still be possible, and if they occur, they are visible through the unmodifiable view.
But let’s say that you want something genuinely unmodifiable, what would you do?
Will the real unmodifiable methods please stand up?
Well, Java 10 adds two new APIs to make this possible, that is, to create collections that can’t be modified at all.
The first API allows unmodifiable copies to be made of collections by adding copyOf:
var unmodifiable = List.copyOf(vegetables);
That is different from wrapping a list in Collections.unmodifiableList in that copyOf performs a shallow copy in iteration order. Changes to vegetables won’t be manifest in unmodifiable now, whereas they are with our original approach.
The second API adds three new methods to the Collectors class in the Stream package. You can now stream directly into an unmodifiable collection using toUnmodifiableList, toUnmodifiableSet, and toUnmodifiableMap:
var result = Arrays.asList(1, 2, 3, 4) .stream() .collect(Collectors.toUnmodifiableList());
Note that while these method names may remind you of Collections.unmodifiableList and the like, these new methods produce genuinely unmodifiable lists, while Collections.unmodifiableList returns an unmodifiable view.
Java 9 made the Garbage-First Garbage Collector (G1GC) the default, replacing the Concurrent Mark-Sweep Garbage Collector (CMS). Java 10 introduces performance improvements to G1GC.
In Java 10, G1GC is getting a performance boost with the introduction of full parallel processing during a Full GC. This change won’t help the best-case performance times of the garbage collector, but it does significantly reduce the worst-case latencies. This makes pauses for garbage collection far less stressful on application performance.
When concurrent garbage collection falls behind, it triggers a Full GC collection. The performance improvement modifies the full collection so it is no longer single-threaded, which significantly reduces the time needed to do a full garbage collection.
Application class-data sharing
Java 5 introduced Class-Data Sharing (CDS) to improve startup times of smaller Java applications.
The general idea was that when the JVM first launched, anything loaded by the bootstrap classloader was serialized and stored in a file on disk that could be reloaded on future launches of the JVM. This meant that multiple instances of the JVM shared the class metadata so it wouldn’t have to load them all every time.
The shared-data cache meant a big improvement in startup times for smaller applications because, in that case, the relative size of the core classes was larger than the application itself.
Java 10 extends this to include the system classloader and the platform classloader. To take advantage of that, you just need to add the following parameter:
Adding your own classes to the archive
But, the bigger change is that it allows you to store your own application-specific classes into the Class-Data Sharing cache, too, possibly further decreasing your startup times.
Basically, it is a three-step process.
The first step is to create the list of classes that should be archived by starting up your application with the appropriate flags and indicating where you want the list to be stored:
java -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=myapp.lst \ -cp $CLASSPATH $MAIN_CLASS
Then, with this list, create a CDS archive:
java -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=myapp.lst \ -XX:SharedArchiveFile=myapp.jsa \ -cp $CLASSPATH
And finally, run your app, using that archive:
java -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=hello.jsa \ -cp $CLASSPATH $MAIN_CLASS
New just-in-time compiler
The Just-In-Time (JIT) Compiler is the part of Java that converts Java bytecode into machine code at runtime. The original JIT Compiler was written in C++ and is now considered quite difficult to modify.
Java 9 introduced a new experimental interface called the JVM Compiler Interface or JVMCI. The design of the new interface makes it possible to rewrite the JIT Compiler in pure Java. Graal is the resulting JIT Compiler, written entirely in Java.
Graal is currently an experimental JIT compiler. Only Linux/x64 machines can use it until future releases of Java.
To enable Graal, add these flags to your command line arguments when starting the application:
And keep in mind that the Graal team makes no promises in this first release that this compiler is any faster. The driving hope is that Graal will help evolve the JVMCI and make future maintenance tractable.
Among the performance improvements in the JVM is a subtle-but-powerful one referred to as Thread-Local Handshakes.
During serviceability operations, like collecting stack traces for all threads or performing garbage collections, when the JVM needed to pause one thread, it needed to stop them all. Sometimes, these are referred to as “stop-the-world” pauses. This was due to the JVM wanting to create a global safepoint from which all application threads could begin again once the JVM was done.
In Java 10, though, the JVM can put an arbitrary number of threads into a safepoint, and threads may continue running after performing the prescribed “handshake”. The result is that if the JVM can pause just one thread at a time, whereas before it had to pause them all.
To be clear, this isn’t a feature directly available to developers, but it is one everyone will enjoy.
A forerunner for big GC changes
And if you’re following closely, you’ll also see that this is related to an upcoming (and experimental) low-latency garbage collector coming in Java 11 which clocks GCs at only 10ms. It’s also a cousin to the very-cool no-GC option coming in Java 11 as well.
The JVM now knows when it is running inside a Docker Container. This means the application now has accurate information about what the docker container allocates to memory, CPU, and other system resources.
Previously, the JVM queried the host operating system to get this information. This causes a problem when the docker container would actually like to advertise a different resource set.
For example, let’s say that you wanted to create a Java-based docker image where the running JVM was allocated 25% of the available memory specified by the container. On a box that has 2G of memory, running a container configured for 0.5G of memory, Java 9 and earlier would incorrectly calculate the Java process’s heap size based on the 2G number instead of 0.5G.
But now in Java 10, the JVM is capable of looking up this information from container control groups (cgroups), which is where Docker places these details.
There are command line options to specify how the JVM inside a Docker container allocates internal memory. For instance, to set the memory heap to the container group size and limit the number of processors you could pass in these arguments:
With containers becoming a standard way to deploy services, this means developers now have a container-based way to control how their Java application uses resources.
Alternative memory allocation
Java is moving toward a more heterogeneous memory system by allowing users to specify alternative memory devices to allocate the heap.
An immediate use case is being able to allocate heap on a Non-Volatile DIMM (NVDIMM) module, which is commonly used in Big Data applications.
Another use case is where many JVM processes are running on the same machine. In this case, it might be good to have processes that require a lower read latency map to DRAM and the remaining processes mapped to NVDIMM.
To use this, add this flag to your startup parameters:
where path would typically be a memory-mapped directory.
Easier SSL with OpenJDK
The open-source version of Java 10, OpenJDK, also received some great news regarding root certificates.
Java ships with a keystore called cacerts which is a home for root certificates for Certificate Authorities that the JVM can use to perform SSL handshakes and the like. But, in OpenJDK, this keystore has always been empty, relying on the user to populate it.
This extra maintenance makes OpenJDK a less attractive choice if your application needs to open SSL sockets.
Basically, this means that now doing simple things like communicating over HTTPS between your application and, say, a Google RESTful service will be much simpler with OpenJDK.
Feel free to check out the difference by using keytool to list the certs in cacerts:
keytool -cacerts -list
If you are using OpenJDK 9 or earlier, this will be empty, but with OpenJDK 10, it will be flush with certificates from Digicert, Comodo, Docusign, and many others.
The new release cycle
Aside from just a project management mechanism, Java 10 actually changes the version numbering schema inside class files.
You’ve all seen an exception like this before:
Unsupported major.minor version 52.0 at java.lang.ClassLoader.defineClass1(Native Method) at java.lang.ClassLoader.defineClass(ClassLoader.java:791) ...
Of course, when you received this exception–if you were keeping track–you knew that this meant that you were trying to run a Java 8 library on a Java 7 JVM or earlier, because 52.0 meant Java 8, just like 51.0 meant Java 7.
Now, though, the new numbering system has semantic meaning. Basically, it is:
FEATURE refers to the version of Java. So, in Java 10’s case, FEATURE is 10. (Makes sense!) It will increment every six-months, matching the new Java release cycle.
INTERIM is actually reserved for future “interim” cycles. For example, if Java wanted to start releasing faster than every six months. For the time being, it will always be 0.
UPDATE is a bit odd. It begins at 0 and one month after the last FEATURE release, it bumps up to 1. And then it increments every three months after that. So, that means that with Java 10, in April 2018, UPDATE was 1. In July 2018, it is 2, and in September, it is 3, incrementing until Java 10 is EOL.
PATCH is any releases that need to happen in between UPDATE increments, for example, critical bug fixes.
Additionally, version numbers remove trailing zeros.
So, that means that the version string when Java 10 went live was simply 10.
In April, Oracle released 10.0.1 and in July, it released 10.0.2. You can check out the release notes for both on their website.
The Java 10 release includes additional bug fixes and performance improvements. The biggest performance boost was in the startup time of the jShell REPL tool. This will make working with the tool more responsive.
Java 10 is the first new release made of the JDK on the new 6-month release cycle.
Each release from now on will have fewer large features, but they will come much faster. This means if a major feature misses a release, it will most likely be released only 6 months later. The original release cycle could have pushed a new feature out several years.
This time, some of the major features of the release were parallelized garbage collection, local variable type-inference, and the new release cycle numbering schema. Finally, for more details be sure to check out the official Java 10 release notes.
- A Guide to Streams in Java 8: In-Depth Tutorial With Examples - March 18, 2020
- SLF4J: 10 Reasons Why You Should Be Using It - November 12, 2018
- What’s New in Java 10 - July 30, 2018
- A Start to Finish Guide to Docker with Java - June 21, 2018
- Exploring Java 9 Module System and Reactive Streams - May 16, 2018