Life after Java 8
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.
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.
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:
- all modules have a unique name
- cyclic dependencies between application modules are not allowed
- split packages are not allowed (two modules with the same package name)
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:
- Avoid using any types from com.sun.tools.javac.util since this package contains private JDK implementation and can be changed in future. This way is preferable.
- Add following instruction to JVM options:
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
. This command will make accessible package com.sun.tools.javac.util to unnamed module (classes from classpath).
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
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.
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.
- Internal APIs of JDK become unavailable (everything related to sun.* is unsupported and can be removed at any time)
- JDK and JRE file structure changes
- Some command line options were removed
- The application class loader is no longer an instance of java.net.URLClassLoader
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.
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.