JDK 14: New hot Features

You are currently viewing JDK 14: New hot Features
JDK 14: New Hot Features

Java 14 (Java SE 14) and its Java Development Kit 14 (JDK 14) open-source has been released on 17 March 2020, the most common coding language and application platform in the world. Oracle now offers Java 14 for all developers and enterprises to download.

Highlights of the latest GA release of standard Java include flight recorder event streaming, switch expressions, NVM support, and records. JDK 14 is a feature release of Java, rather than a long-term support (LTS) release, following the six-month release cadence set for Java. JDK 14 will receive security updates in April and July before being superseded by JDK 15, also a non-LTS release, which is due in September. The current LTS release is JDK 11.

1. Pattern Matching for instanceof

Goal: Enhance the Java programming language with pattern matching for the instanceof operator. Pattern matching allows common logic in a program, namely the conditional extraction of components from objects, to be expressed more concisely and safely. This is a preview language feature in JDK 14.

A pattern is a combination of (1) a predicate that can be applied to a target, and (2) a set of binding variables that are extracted from the target only if the predicate successfully applies to it.

A type test pattern consists of a predicate that specifies a type, along with a single binding variable.

The instanceof operator (JLS 15.20.2) is extended to take a type test pattern instead of just a type. In the code below, the phrase String s is the type test pattern:

if (obj instanceof String s) {
    // can use s here
} else {
    // can't use s here
}

The instanceof operator “matches” the target obj to the type test pattern as follows: if obj is an instance of String, then it is cast to String and assigned to the binding variable s. The binding variable is in scope in the true block of the if statement, and not in the false block of the if statement.

The scope of a binding variable, unlike the scope of a local variable, is determined by the semantics of the containing expressions and statements.

2. Packaging Tool (Incubator)

The jpackage tool packages a Java application into a platform-specific package that includes all of the necessary dependencies. The application may be provided as a collection of ordinary JAR files or as a collection of modules. The supported platform-specific package formats are:

  • Linux: deb and rpm
  • macOS: pkg and dmg
  • Windows: msi and exe

By default, jpackage produces a package in the format most appropriate for the system on which it is run.

Basic usage: Non-modular applications
Suppose you have an application composed of JAR files, all in a directory named lib, and that lib/main.jar contains the main class. Then the command

$ jpackage –name myapp –input lib –main-jar main.jar
will package the application in the local system’s default format, leaving the resulting package file in the current directory. If the MANIFEST.MF file in main.jar does not have a Main-Class attribute then you must specify the main class explicitly:

$ jpackage –name myapp –input lib –main-jar main.jar \
–main-class myapp.Main
The name of the package will be myapp, though the name of the package file itself will be longer, and end with the package type (e.g., myapp.exe). The package will include a launcher for the application, also called myapp. To start the application, the launcher will place every JAR file that was copied from the input directory on the class path of the JVM.

If you wish to produce a package in a format other than the default, then use the –type option. For example, to produce a pkg file rather than dmg file on macOS:

$ jpackage –name myapp –input lib –main-jar main.jar –type pkg
Basic usage: Modular applications
If you have a modular application, composed of modular JAR files and/or JMOD files in a lib directory, with the main class in the module myapp, then the command

$ jpackage –name myapp –module-path lib -m myapp
will package it. If the myapp module does not identify its main class then, again, you must specify that explicitly:

$ jpackage –name myapp –module-path lib -m myapp/myapp.Main
(When packaging a modular JAR or a JMOD file you can specify the main class with the –main-class option to the jar and jmod tools.)

3. NUMA-Aware Memory Allocation for G1

G1’s heap is organized as a collection of fixed-size regions. A region is typically a set of physical pages, although when using large pages (via -XX:+UseLargePages) several regions may make up a single physical page.

If the +XX:+UseNUMA option is specified then, when the JVM is initialized, the regions will be evenly spread across the total number of available NUMA nodes.

Fixing the NUMA node of each region at the beginning is a bit inflexible, but this can be mitigated by the following enhancements. In order to allocate a new object for a mutator thread, G1 may need to allocate a new region. It will do so by preferentially selecting a free region from the NUMA node to which the current thread is bound, so that the object will be kept on the same NUMA node in the young generation. If there is no free region on the same NUMA node during region allocation for a mutator then G1 will trigger a garbage collection. An alternative idea to be evaluated is to search other NUMA nodes for free regions in order of distance, starting with the closest NUMA node.

4. JFR Event Streaming

The package jdk.jfr.consumer, in module jdk.jfr, is extended with functionality to subscribe to events asynchronously. Users can read recording data directly, or stream, from the disk repository without dumping a recording file. The way to interact with a stream is to register a handler, for example a lambda function, to be invoked in response to the arrival of an event.

The following example prints the overall CPU usage and locks contended for more than 10 ms.

try (var rs = new RecordingStream()) {
  rs.enable("jdk.CPULoad").withPeriod(Duration.ofSeconds(1));
  rs.enable("jdk.JavaMonitorEnter").withThreshold(Duration.ofMillis(10));
  rs.onEvent("jdk.CPULoad", event -> {
    System.out.println(event.getFloat("machineTotal"));
  });
  rs.onEvent("jdk.JavaMonitorEnter", event -> {
    System.out.println(event.getClass("monitorClass"));
  });
  rs.start();
}

The RecordingStream class implements the interface jdk.jfr.consumer.EventStream that provides a uniform way to filter and consume events regardless if the source is a live stream or a file on disk.

public interface EventStream extends AutoCloseable {
  public static EventStream openRepository();
  public static EventStream openRepository(Path directory);
  public static EventStream openFile(Path file);
  void setStartTime(Instant startTime);
  void setEndTime(Instant endTime);
  void setOrdered(boolean ordered);
  void setReuse(boolean reuse);
  void onEvent(Consumer<RecordedEvent> handler);
  void onEvent(String eventName, Consumer<RecordedEvent handler);
  void onFlush(Runnable handler);
  void onClose(Runnable handler);
  void onError(Runnable handler);
  void remove(Object handler);
  void start();
  void startAsync();
  void awaitTermination();
  void awaitTermination(Duration duration);
  void close();
}

There are three factory methods to create a stream. EventStream::openRepository(Path) constructs a stream from a disk repository. This is a way to monitor other processes by working directly against the file system. The location of the disk repository is stored in the system property “jdk.jfr.repository” that can be read using the attach API. It is also possible to perform in-process monitoring using the EventStream::openRepository() method. Unlike RecordingStream, it does not start a recording. Instead, the stream receives events only when recordings are started by external means, for example using JCMD or JMX. The method EventStream::openFile(Path) creates a stream from a recording file. It complements the RecordingFile class that already exists today.

The interface can also be used to set the amount of data to buffer and if events should be ordered chronologically. To minimize allocation pressure, there is also an option to control if a new event object should be allocated for each event, or if a previous object can be reused. A stream can be started in the current thread or asynchronously.

Events stored in thread-local buffers are flushed periodically to the disk repository by the Java Virtual Machine (JVM) once every second. A separate thread parses the most recent file, up to the point in which data has been written, and pushes the events to subscribers. To keep overhead low, only actively subscribed events are read from the file. To receive a notification when a flush is complete, a handler can be registered using the EventStream::onFlush(Runnable) method. This is an opportunity to aggregate or push data to external systems while the JVM is preparing the next set of events.

5. Non-Volatile Mapped Byte Buffers

Preliminary Changes
This JEP makes use of two related enhancements to the Java SE API:

  1. Support implementation-defined Map Modes (JDK-8221397)
  2. MappedByteBuffer::force method to specify range (JDK-8221696)

Proposed JDK-Specific API Changes
1. Expose new MapMode enumeration values via a public API in a new module
A new module, jdk.nio.mapmode, will export a single new package of the same name. A public extension enumeration ExtendedMapMode will be added to this package:

public class ExtendedMapMode {
    private ExtendedMapMode() { }

    public static final MapMode READ_ONLY_SYNC = . . .
    public static final MapMode READ_WRITE_SYNC = . . .
}

The new enumeration values are used when calling the FileChannel::map method to create, respectively, a read-only or read-write MappedByteBuffer mapped over an NVM device file. An UnsupportedOperationException will be thrown if these flags are passed on platforms which do not support mapping of NVM device files. On supported platforms, it is only appropriate to pass these new values as arguments when the target FileChannel instance is derived from a file opened via an NVM device. In any other case an IOException will be thrown.

2. Publish a BufferPoolMXBean tracking persistent MappedByteBuffer statistics

The ManagementFactory class provides method List getPlatformMXBeans(Class) which can be used to retrieve a list of BufferPoolMXBean instances tracking count, total_capacity and memory_used for the existing categories of mapped or direct byte buffers. It will be modified to return an extra, new BufferPoolMXBean with name “mapped – ‘non-volatile memory'”, which will track the above stats for all MappedByteBuffer instances currently mapped with mode ExtendedMapMode.READ_ONLY_SYNC or ExtendedMapMode.READ_WRITE_SYNC. The existing BufferPoolMXBean with name mapped will continue only to track stats for MappedByteBuffer instances currently mapped with mode MapMode.READ_ONLY, MapMode.READ_WRITE or MapMode.PRIVATE.

6. Helpful NullPointerExceptions

The JVM throws a NullPointerException (NPE) at the point in a program where code tries to dereference a null reference. By analyzing the program’s bytecode instructions, the JVM will determine precisely which variable was null, and describe the variable (in terms of source code) with a null-detail message in the NPE. The null-detail message will then be shown in the JVM’s message, alongside the method, filename, and line number.

Note: The JVM displays an exception message on the same line as the exception type, which can result in long lines. For readability in a web browser, this JEP shows the null-detail message on a second line, after the exception type.

For example, an NPE from the assignment statement a.i = 99; would generate this message:

Exception in thread "main" java.lang.NullPointerException: 
        Cannot assign field "i" because "a" is null
    at Prog.main(Prog.java:5)

If the more complex statement a.b.c.i = 99; throws an NPE, the message would dissect the statement and pinpoint the cause by showing the full access path which led up to the null:

Exception in thread "main" java.lang.NullPointerException: 
        Cannot read field "c" because "a.b" is null
    at Prog.main(Prog.java:5)

7. Switch Expressions (Standard)

In addition to traditional “case L :” labels in a switch block, JDK 14 define a new simplified form, with “case L ->” labels. If a label is matched, then only the expression or statement to the right of the arrow is executed; there is no fall through. For example, given the following switch statement that uses the new form of labels:

static void howMany(int k) {
    switch (k) {
        case 1  -> System.out.println("one");
        case 2  -> System.out.println("two");
        default -> System.out.println("many");
    }
}

The following code:

howMany(1);
howMany(2);
howMany(3);

results in the following output:

one
two
many

Switch Expression
Now switch statement is extended so it can be used as an expression. For example, the previous howMany method can be rewritten to use a switch expression, so it uses only a single println.

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
};

A switch expression is a poly expression; if the target type is known, this type is pushed down into each arm. The type of a switch expression is its target type, if known; if not, a standalone type is computed by combining the types of each case arm.

Most switch expressions will have a single expression to the right of the “case L ->” switch label. In the event that a full block is needed, JDK has introduce a new yield statement to yield a value, which becomes the value of the enclosing switch expression.

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

A switch expression can, like a switch statement, also use a traditional switch block with “case L:” switch labels (implying fall through semantics). In this case, values are yielded using the new yield statement:

int result = switch (s) {
    case "Foo": 
        yield 1;
    case "Bar":
        yield 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        yield 0;
};

8. Remove the Concurrent Mark Sweep (CMS) Garbage Collector

This change will disable compilation of CMS, remove the contents of the gc/cms directory in the source tree, and remove options that pertain solely to CMS. References to CMS in the documentation will also be purged. Tests that try to use CMS will be removed or adapted as necessary.

Trying to use CMS via the -XX:+UseConcMarkSweepGC option will result in the following warning message and the VM will continue execution using the default collector.:

Java HotSpot(TM) 64-Bit Server VM warning: Ignoring option UseConcMarkSweepGC; support was removed in <version>

9. ZGC on macOS

The macOS implementation of ZGC consists of two parts:

  • Support for multi-mapping memory on macOS. The ZGC design makes intensive use of colored pointers, so JDK 14 need a way on macOS to map multiple virtual addresses (comprising different colors in the algorithm) to the same physical memory. JDK 14 will use the mach microkernel mach_vm_remap API for this. The physical memory of the heap is maintained in a separate address view, conceptually similar to a file descriptor, but residing in a (mostly) contiguous virtual address instead. This memory is remapped into the various ZGC views of memory, representing the different pointer colors of the algorithm.
  • Support in ZGC for discontiguous memory reservations. On Linux, JDK 14 reserve 16TB of virtual address space during initialization. For that to work, JDK 14 assume that no shared libraries will be mapped into the desired address space. On a default Linux configuration, that is a safe assumption to make. However, on macOS, the ASLR mechanism intrudes into our address space, so ZGC must allow the heap reservation to be discontiguous. The shared VM code must also stop assuming that a single contiguous memory reservation is used by a GC implementation. As a result, GC APIs such as is_in_reserved(), reserved_region() and base() will be removed from CollectedHeap.

10. ZGC on Windows

Most of the ZGC code base is platform independent and requires no Windows-specific changes. The existing load barrier support for x64 is operating-system agnostic and can also be used on Windows. The platform specific code that needs to be ported relates to how address space is reserved and how physical memory is mapped into a reserved address space. The Windows API for memory management differs from the POSIX API and is less flexible in some ways.

The Windows implementation of ZGC requires the following work:

  • Support for multi-mapping memory. ZGC’s use of colored pointers requires support for heap multi-mapping, so that the same physical memory can be accessed from multiple different locations in the process address space. On Windows, paging-file backed memory provides physical memory with an identity (a handle), which is unrelated to the virtual address where it is mapped. Using this identity allows ZGC to map the same physical memory into multiple locations.
  • Support for mapping paging-file backed memory into a reserved address space. The Windows memory management API is not as flexible as POSIX’s mmap/munmap, especially when it comes to mapping file backed memory into a previously reserved address space region. To do this, ZGC will use the Windows concept of address space placeholders. The placeholder concept was introduced in version 1803 of Windows 10 and Windows Server. ZGC support for older versions of Windows will not be implemented.
  • Support for mapping and unmapping arbitrary parts of the heap. ZGC’s heap layout in combination with its dynamic sizing (and re-sizing) of heap pages requires support for mapping and unmapping arbitrary heap granules. This requirement in combination with Windows address space placeholders requires special attention, since placeholders must be explicitly split/coalesced by the program, as opposed to being automatically split/coalesced by the operating system (as on Linux).
  • Support for committing and uncommitting arbitrary parts of the heap. ZGC can commit and uncommit physical memory dynamically while the Java program is running. To support these operations the physical memory will be divided into, and backed by, multiple paging-file segments. Each paging-file segment corresponds to a ZGC heap granule, and can be committed and uncommitted independently of other segments.

11. Deprecate the ParallelScavenge + SerialOld GC Combination

In addition to deprecating the option combination -XX:+UseParallelGC -XX:-UseParallelOldGC JDK 14 will also deprecate the option -XX:UseParallelOldGC, since its only use is to deselect the parallel old generation GC, thereby enabling the serial old generation GC.

As a result, any explicit use of the UseParallelOldGC option will display a deprecation warning. A warning will, in particular, be displayed when -XX:+UseParallelOldGC is used standalone (without -XX:+UseParallelGC) to select the parallel young and old generation GC algorithms.

The only way to select the parallel young and old generation GC algorithms without a deprecation warning will to specify only -XX:+UseParallelGC on the command line.

12. Text Blocks

A text block is a new kind of literal in the Java language. It may be used to denote a string anywhere that a string literal could appear, but offers greater expressiveness and less accidental complexity.

A text block consists of zero or more content characters, enclosed by opening and closing delimiters.

The opening delimiter is a sequence of three double quote characters (“””) followed by zero or more white spaces followed by a line terminator. The content begins at the first character after the line terminator of the opening delimiter.

The closing delimiter is a sequence of three double quote characters. The content ends at the last character before the first double quote of the closing delimiter.

The content may include double quote characters directly, unlike the characters in a string literal. The use of \” in a text block is permitted, but not necessary or recommended. Fat delimiters (“””) were chosen so that ” characters could appear unescaped, and also to visually distinguish a text block from a string literal.

The content may include line terminators directly, unlike the characters in a string literal. The use of \n in a text block is permitted, but not necessary or recommended. For example, the text block:

“””
line 1
line 2
line 3
“””
is equivalent to the string literal:

"line 1\nline 2\nline 3\n"

or a concatenation of string literals:

"line 1\n" +
"line 2\n" +
"line 3\n"

If a line terminator is not required at the end of the string, then the closing delimiter can be placed on the last line of content. For example, the text block:

"""
line 1
line 2
line 3"""

A text block can denote the empty string, although this is not recommended because it needs two lines of source code:

String empty = """
""";

Here are some examples of ill-formed text blocks:

String a = """""";   // no line terminator after opening delimiter
String b = """ """;  // no line terminator after opening delimiter
String c = """
           ";        // no closing delimiter (text block continues to EOF)
String d = """
           abc \ def
           """;      // unescaped backslash 

13. Foreign-Memory Access API (Incubator)

The foreign-memory access API introduces three main abstractions: MemorySegment, MemoryAddress and MemoryLayout.

A MemorySegment is used to model a contiguous memory region with given spatial and temporal bounds. A MemoryAddress can be thought of as an offset within a segment. Finally, a MemoryLayout is a programmatic description of a memory segment’s contents.
Memory segments can be created from a variety of sources, such as native memory buffers, Java arrays, and byte buffers (either direct or heap-based). For instance, a native memory segment can be created as follows:

try (MemorySegment segment = MemorySegment.allocateNative(100)) {
   ...
}

This will create a memory segment that is associated with a native memory buffer whose size is 100 bytes.

Dereferencing the memory associated with a segment can be achieved by obtaining a memory-access var handle. These special var handles have at least one mandatory access coordinate, of type MemoryAddress, which is the address at which the dereference occurs. They are obtained using factory methods in the MemoryHandles class. For instance, to set the elements of a native segment we could use a memory-access var handle as follows:

VarHandle intHandle = MemoryHandles.varHandle(int.class,
        ByteOrder.nativeOrder());
try (MemorySegment segment = MemorySegment.allocateNative(100)) {
    MemoryAddress base = segment.baseAddress();
    for (int i = 0; i < 25; i++) {
        intHandle.set(base.addOffset(i * 4), i);
    }
}

To enhance the expressiveness of the API, and to reduce the need for explicit numeric computations such as those in the above examples, the MemoryLayout API can be used to programmatically describe the content of a memory segment. For instance, the layout of the native memory segment used in the above examples can be described in the following way:

SequenceLayout intArrayLayout
    = MemoryLayout.ofSequence(25,
        MemoryLayout.ofValueBits(32,
            ByteOrder.nativeOrder()));

VarHandle intElemHandle
    = intArrayLayout.varHandle(int.class,
        PathElement.sequenceElement());

try (MemorySegment segment = MemorySegment.allocateNative(intArrayLayout)) {
    MemoryAddress base = segment.baseAddress();
    for (int i = 0; i < intArrayLayout.elementCount().getAsLong(); i++) {
        intElemHandle.set(base, (long) i, i);
    }
}

In this example, the layout instance drives the creation of the memory-access var handle through the creation of a layout path, which is used to select a nested layout from a complex layout expression. The layout instance also drives the allocation of the native-memory segment, which is based upon size and alignment information derived from the layout. The loop constant in the previous examples has been replaced with the sequence layout’s element count.

The foreign-memory access API will initially be provided as an incubating module, named jdk.incubator.foreign, in a package of the same name.

References:

https://openjdk.java.net/projects/jdk/14/

Leave a Reply here