Life after Java 8

June 15, 2018

In March 2018 Java 10 was released. But a lot of projects haven't adopted even Java 9. I hope this article will help you figure out whether you should try recent releases or stay with current.

Before we get started I'd like to mention that latest stable Java release (I mean Java 8) is not perfect.

First, we have to work with massive monolithic JRE. Picture below shows dependencies between main JRE components and how many of them are used to run "hello world" application.

JRE 7 dependencies

Such bounded system became hard to maintain and caused ineffective resources utilization.

The second problem is error prone classpath. There is not guarantee that we did not forget something when launch the application. Also if somehow we got two versions of the same class in the classpath we can't be sure which one will be picked up at runtime.

The last but not least problem I'd like to mention is encapsulation. If we want one class to be accessible from another package we have to declare it as public. But this class (as well as a package) might be a part of some internal API.

JDK 9

Jigsaw

Modular system (project Jigsaw) was introduced in Java 9 to solve these (and not only) problems. So JRE is no longer monolithic but a set of modules. The picture below is called module graph. java.base is the root module. This is where java.lang.Object lives and other important classes. Any other module depends on java.base directly or indirectly.

Java EE module graph

The modules provided by JRE itself are called platform modules. Prior to Java 9, JRE classes were located in the rt.jar. This file does not exist anymore and each module is in its own file with *.jmod extension.

So what is module? Module, essentially, is a regular jar file with module-info.class in it's root. The source tree will look like this.

├── src/
│   ├── main/
│   │   │── java/
│   │   │   ├── com/
│   │   │   └── module-info.java
├── ... ...

This file is called module descriptor.

module module.name {   //unique module name

    requires another.module; //another module that our module depends on

    exports module.api; //public types of this package can be accessible from any other modules

    exports module.imp to util.module; //public types of this package can be accessible only from "util.module". 
                                      // The same we can do for "opes" statement.

    opens module.reflection; //all types of this package can be accessible with reflection at runtime (but not at compile)
}

It's worth to note that requires statement works with module names. exports and opes, in turn, work with package names.

As opposed to classpath Java 9 introduced concept of module-path (but classpath still works). Module-path specifies where JVM should look for modules. It has some differences from classpath:

To help with migration from classpath to module-path Jigsaw project announced concept of Unnamed and Automatic modules.

On the table below you can see all module types:

Module type Origin Exports packages Can read modules
Platform provided by JDK explicitly Platform
Unnamed any JAR in the classpath All All
Application any JAR with module-info in the module path explicitly Platform Application Automatic
Automatic any JAR without module-info in the module path All All

If we try to compile or run your existing application via classpath in Java 9 or above - all classes will be put into so-called Unnamed module. Our classes will be able to interact between each other as we got used to.

However our classes won't be able to access packages which are not exported by platform modules. You will see something like this:

cannot access class com.sun.tools.javac.util.Context (in module jdk.compiler) 
because module jdk.compiler does not export com.sun.tools.javac.util to unnamed module

This exception means that we try to use in our code class com.sun.tools.javac.util.Context, but module jdk.compiler does't have the following clause in it's module-info.class:

exports com.sun.tools.javac.util

There are two possible ways to solve this problem:

The same trick you can do if you try to access "not opened" packages of JDK with reflection, e.g.

Unable to make protected java.lang.Package[] java.lang.ClassLoader.getPackages() accessible:
 module java.base does not "opens java.lang" to unnamed module

The permanent solution - do not rely on JDK internals. The temporary solution - add JVM option: --add-opens java.base/java.lang=ALL-UNNAMED

Jigsaw suggests several command line options to help with migration:

--add-exports ~ exports

--add-opens   ~ opens

--add-reads   ~ requires

Jigsaw developers understood that migration to modules (from classpath to module-path) must be a gradual process. For migration purposes they suggested a concept of automatic modules.

Automatic module - is a regular jar file without module-info.class which we put in module-path. As I mentioned above - module-path does not accept split packages. This is the main requirement for automatic modules. These automatic modules will also get a name either from manifest (Automatic-Module-Name property) or from jar-file name. Manifest option is preferable way especially for library maintainers.

Automatic module can access both Unnamed module and Application modules (with module-info.class). If your application module uses some types from automatic module - you must specify it in module-info, e.g. requires automatic.module.name. Or you can use --add-reads option as a temporary solution if for some reasons you don't have access to module's source code.

As I pointed out earlier rt.jar has gone and platform modules are stored in a new *.jmod format in jmods directory. The general file structure also changed

JDK file structure changes

Modular system gave the second breath for service-loading mechanism (using java.util.ServiceLoader). It allows to separate API and implementation into different modules. By the way service-loading mechanism allowed to resolve cyclic dependencies between platform modules.

Service registration

This is how service declaration looks like

module service.module {
    exports com.service.module.api;
}

module provider.module {

  requires service.module; // this is module where desired service lives

  provides  com.service.module.api.GreetingsService with com.service.provider.GreetingsServiceImpl; 
  // module provides implementation of GreetingsService (implements interface or extends class)
}

module consumer.module {

  requires service.module; 

  uses com.service.module.api.GreetingsService; // module anticipates implementations of GreetingsService 
}

In java code you can load GreetingsService this way

ServiceLoader<GreetingsService> services = ServiceLoader.load(GreetingsService.class);

Jlink

To take advantages of modular system JDK developers introduced Jlink command-line tool. It allows to create runtime-images and include there only necessary modules. For example to create the simplest application we don't need any other modules except java.base.

jlink --module-path "%JAVA_HOME%/jmods" \ # specify jmods directory of target JDK (to include necessary platform modules) 
                                          # and other modules location if needed
--add-modules java.base \ # specify which modules to include (or just single top level module)
--output folder-name \ # where to put runtime image
--compress=2 # compression rate (can be 0,1,2)

In the example above we will get fully operational JVM which does not require installation and include classes of java.base module. The size of this image (JVM + classes) will be about 25 MB.

If we have fully modularized application (all modules have module-info.class) we can add launcher to it:

 --launcher startApp=main.module/com.jlink.app.MainClass 

The advantages of such runtime images are obvious - we can make our applications smaller and they can be "shipped and launched" (no need to install JRE separately). Also we can create runtime images for different platforms, e.g. on Linux for Windows etc. To do so we need to have downloaded JDK of the target platform and use its location as %JAVA_HOME% in the example above.

Some Other Java 9 Features

Compact Strings

The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. Data gathered from many different applications indicates that strings are a major component of heap usage and, moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, hence half of the space in the internal char arrays of such String objects is going unused. Since Java 9 this problem will be solved.

HTTP/2 Client

Provided a new HTTP client API that implements HTTP/2 and WebSocket, and can replace the legacy HttpURLConnection API. Offers performance on par with Netty and Jetty when used as a client API for HTTP/2.

Multi-Release JAR Files

Extended the JAR file format to allow multiple, Java-release-specific versions of class files to coexist in a single archive.

jar root
├──A.class
├──B.class
├──C.class
├──D.class
└──META-INF
   └── versions/
       │── 9/
       │   ├── A.class
       │   └── B.class
       └── 10/
           └── A.class

The full list of improvements in JDK 9 is much bigger and you can find it here.

JDK 10

JDK 10 can't brag about the number of improvements in comparison with JDK 9. Let's have a brief review of some of them.

Local-Variable Type Inference

The goal of this enhancement was to improve the developer experience by reducing the ceremony associated with writing Java code.

So now instead of

// explicit types
No no = new No();
AmountIncrease<BigDecimal> more = new BigDecimalAmountIncrease();
HorizontalConnection<LinePosition, LinePosition> jumping =
       new HorizontalLinePositionConnection();
Variable variable = new Constant(5);
List<String> names = List.of("Hello", "world");

you can write something like this

// inferred types
var no = new No();
var more = new BigDecimalAmountIncrease();
var jumping = new HorizontalLinePositionConnection();
var variable = new Constant(5);
var names = List.of("Hello", "world");

This treatment would be restricted to local variables with initializers, indexes in the enhanced for-loop, and locals declared in a traditional for-loop; it would not be available for method formals, constructor formals, method return types, fields, catch formals, or any other kind of variable declaration.

Heap Allocation on Alternative Memory Devices

In JDK 10 was added support to allocate the Java object heap on an alternative memory device, such as an NV-DIMM. Low-priority processes can use NV-DIMM memory for the heap, allowing high-priority processes to use more DRAM. Applications such as big data and in-memory databases have an ever-increasing demand for memory. Such applications could use NV-DIMM for the heap since NV-DIMMs would potentially have a larger capacity, at lower cost, compared to DRAM.

There are 12 JEPs in JDK 10. You can find the full list here.

Breaking Changes

Unfortunately big changes entail breaking changes. Most of troubles are coming from JDK 9, so you need to be ready for them.

This list is not complete. For more information, please visit JDK 9 migration page.

Third Party Support

It's hard to talk about adoption without frameworks and libraries support. Many popular vendors have already announced their "Java 9 - compatibility". I think this situation won't change significantly for Java 10. Third party support

Summary

Java 9 and 10 introduced a lot of cool features including modularity, compact strings, performance improvement etc. So at least new projects should start development "in a modular way".

If your application does not rely on JDK internal APIs - it should work almost without changes. For applications that heavily rely on JDK internals and have split packages, migration to modules can become a huge challenge. However, JDK community provided a lot of things to help with migration.

Java 9,10 are not LTS releases, Java 11 (2018/09/25 General Availability) is expected to be the first LTS release after Java 8.

About the author: David Shevchenko