Java 27

Better Language, Better APIs, Better Runtime

Developer Advocate

Java Team at Oracle

Let’s get started!

JDK 27 was released in September:
jdk.java.net/27

This session:

  • briefly summarizes Java 22 to 25

  • goes into more detail on Java 26 and 27

  • time permitting, peeks into Java 28

  • is a showcase, not a tutorial

Slides at slides.nipafx.dev/java-x (⇝ hit "?")

Ask questions at any time!

Java 27

Java 22 to 25
Language Features
New APIs
Runtime Improvements
Better Tooling
...

Java 27

Java 22 to 25
Language Features
New APIs
Runtime Improvements
Better Tooling
…​

Flexible Constructor Bodies

class ThreePartName extends Name {
	private final String middle;

	ThreePartName(String full) {
		// split "first middle last" on space
		var names = full.split(" ");
		// assign fields before `super`
		this.middle = names[1];
		// call `Name(String first, String last)`
		super(names[0], names[2]);
	}
}

Unnamed patterns

Use _ to mark a (pattern) variable as unused, e.g.:

BiConsumer<String, Double> = (s, _) -> // use `s`

Object obj = // ...
if (obj instanceof User(var name, _))
	// use `name`

switch (obj) {
	case User _ -> userCount++;
	case Admin _ -> adminCount++;
}

Module Imports

import module $mod;
  • imports public API of $mod

  • your code does not need to be in a module

Simplified Main

Get started quicker:

// entire source file
// implicit `import module java.base`
void main() {
    var name = IO.readln("Please enter your name: ");
    IO.println("Nice to meet you, " + name);
}

(Execute with: java Main.java)

Java 27

Java 22 to 25
Language Features
New APIs
Runtime Improvements
Better Tooling
…​

Stream Gatherers

Create custom stream operations:

Stream.of("A", "C", "F", "B", "S")
	.gather(groups(2))
	.forEach(IO::println);

// [A, C]
// [F, B]
// [S]

Scoped Values

Simpler, more scalable, one-way alternative to ThreadLocal:

static final ScopedValue<Integer> ANSWER =
	ScopedValue.newInstance();

void main() {
	ScopedValue //      ⬐ VALUE
		.where(ANSWER, 42)
		//  |<---------- SCOPE ----------->|
		.run(() -> IO.println(ANSWER.get())); // "42"

	// OUT OF SCOPE
	ANSWER.get(); // ⚡️ NoSuchElementException
}

Foreign-memory API

Interact with off-heap memory:

// create `Arena` to manage off-heap memory lifetime
try (Arena offHeap = Arena.ofConfined()) {
	// [allocate off-heap memory to store pointers]
	// [do something with off-heap data]
	// [copy data back to heap]
} // off-heap memory is deallocated here

Foreign-function API

Interact with native libraries:

// find foreign function on the C library path
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MethodHandle radixSort = linker
	.downcallHandle(stdlib.find("radixsort"), ...);

Class-file API

A modern, on-board bytecode manipulation API:

  • allows frameworks and libraries
    to drop dependency on ASM et al.

  • removes a big reason for:

Update all dependencies before updating the JDK.

Smaller API Additions

  • Console: Locale overloads for
    format, printf, readLine, readPassword

  • Console: isTerminal

  • Reader: readLines, readAllAsString

  • Math, StrictMath: more exact overloads,
    e.g. unsignedMultiplyExact(int, int)

  • Instant: until(Instant)

  • ForkJoinPool: schedule…​ and more

  • BodyHandlers, BodySubscribers: limiting

Java 27

Java 22 to 25
Language Features
New APIs
Runtime Improvements
Better Tooling
…​

Multi-File Execution

Small programs can expand:

MyFirstJava
 ├─ Main.java
 ├─ Helper.java
 └─ Lib
     └─ library.jar

Run with:

java -cp 'Lib/*' Main.java

AOT computation

Reduce your applications' startup and warmup times
with ahead-of-time computation:

# training run (⇝ AOTCache)
$ java -XX:AOTCacheOutput=app.aot
       -cp app.jar com.example.App ...
# production run (AOTCache ⇝ performance)
$ java -XX:AOTCache=app.aot
       -cp app.jar com.example.App ...

Compact object headers

Reduce object headers from (usually) 12 bytes to 8 bytes:

-XX:+UseCompactObjectHeaders
  • reduces heap size by 5-30%

  • can reduce garbage collections

  • can improve or deteriorate
    overall performance

Report observations to hotspot-dev.

Generational ZGC

netflix genzgc gc pause

Less virtual thread pinning

Virtual threads:

  • execute on a platform thread

  • usually unmount from PT when waiting

  • pinning prevents that

  • caused by native calls, class initialization, and
    object monitors (e.g. synchronized)

Object monitors were reimplemented.

⇝ No more pinning for synchronized.

Performance improvements

Every Java release improves performance, e.g.:

  • +10%/+5% critical/max jOPS in SPECjbb 2015

  • +70-75% requests/s on Helidon

(JDK 25 vs 21)

Security enhancements

Many enhancements between Java 21 and 25:

Java 27

Java 22 to 25
Language Features
New APIs
Runtime Improvements
Better Tooling
…​

JFR improvements

JDK Flight Recorder got better:

  • cooperative sampling (JEP 518)

  • method timing & tracing (JEP 520)

  • CPU-time profiling (JEP 509, experimental)

Markdown in Javadoc

/// Returns `true` if, and only if,
/// [#length()] is `0`.
///
/// @return `true` if [#length()] is `0`,
//          otherwise `false`
/// @since 1.6
@Override
public boolean isEmpty() { /* ... */ }

More

Specifically:

Generally:

Java 27

Java 22 to 25
Java 26
HTTP/3
The Rest
Java 27
Deprecations & Removals
Java 28

HTTP client

Java’s HTTP API:

HttpClient client = HttpClient.newBuilder()
	.version(HTTP_2)
	.build();

HttpRequest request = HttpRequest.newBuilder()
	.uri(URI.create("https://dev.java"))
	.build();

var response = client
	.send(request, BodyHandlers.ofString());

HTTP/3

It now supports HTTP/3:

HttpClient client = HttpClient.newBuilder()
	.version(HTTP_3)
	.build();

HttpRequest request = HttpRequest.newBuilder()
	.uri(URI.create("https://dev.java"))
	.version(HTTP_3)
	.build();

Connection upgrades

Network stacks:

  • HTTP/1.1 and 2 use TCP

  • HTTP/3 uses QUIC over UDP

⇝ Can’t upgrade a connection to /3.

Version selection

Four options:

  1. start with /2
    ⇝ use /3 for next request if supported

  2. start with /2 and /3
    ⇝ keep using what replies first

  3. start with /3
    ⇝ on timeout, request with /2

  4. insist on /3
    ⇝ otherwise fail

Check JEP or documentation for details.

More

Final in JDK 26.

Java 27

Java 22 to 25
Java 26
HTTP/3
The Rest
Java 27
Deprecations & Removals
Java 28

Rest of Java 26

Java 27

…​
Java 27
Compact Object Headers
Lazy Constants
Structured Concurrency
PEM Encoding
The Rest
…​

Object information

Runtime needs to track information per object:

  • garbage collection: forwarding pointers and object ages

  • type system: object’s class

  • locking: unlocked, light-weight, or heavy-weight locks

  • hashing: identity hash code, once computed

This is stored in the object header.

Object header

On 64bit hardware:

95-64 .........................HHHHHHH
63-32 HHHHHHHHHHHHHHHHHHHHHHHH.AAAA.TT
31- 0 CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
  • A: GC age

  • C: class word (compressed)

  • H: hash code

  • T: tag bits

Object Overhead

This incurs overhead:

  • objects tend to be small
    (many workloads average 256-512 bits)

  • header size is noticable
    (15-30% on such workloads)

⇝ This is worth optimizing.

Complications

The aforementioned subsystems are
complicated and performance-sensitive.

I’ve only told you the easy part of object headers.

Compressed headers

Project Lilliput achieved its first milestone:

63-32 CCCCCCCCCCCCCCCCCCCCCCHHHHHHHHHH
31- 0 HHHHHHHHHHHHHHHHHHHHHVVVVAAAASTT
  • A: GC age

  • C: class word

  • H: hash code

  • S: self-forwarding

  • T: tag bits

  • V: Valhalla bits

Effects

These changes have several downstream effects
for the complexities I (didn’t) mention.

In special cases, these may be detrimental to performance.

Benchmarks

Configuration

Compact object headers:

  • Java 24 experiments

  • Java 25 finalizes

  • Java 27 enables by default

Configure with flag:

# disable on JDK 27
-XX:-UseCompactObjectHeaders

Note

Interaction with other features:

  • compressed class pointers must be enabled (default)

  • stack locking must be disabled (default)

More

Java 27

…​
Java 27
Compact Object Headers
Lazy Constants
Structured Concurrency
PEM Encoding
The Rest
…​

Laziness

Deferring computation is interesting…​

  • if it takes long

  • if later execution creates better results

  • if we may not need to do it

But has tradeoffs:

  • more complexity

  • increased tail-end latency

Java and our programs are lazy in many regards.

Laziness in Java

Program infrastructure is built on fields.

Lazy field initialization:

  • is harder to get right

  • prevents final:

    • less maintainable

    • no constant folding

Double-checked Locking

private volatile LoginService login;

private LoginService getLogin() {
	var login = this.login;
	if (login == null) {
		synchronized (this) {
			login = this.login;
			if (login == null)
				this.login = login =
					LoginService.initialize();
		}
	}
	return login;
}

Lazy constants

public class UserController {

	private final LazyConstant<LoginService> login;

	public UserController() {
		this.login = LazyConstant
			.of(LoginService::initialize);
		// using `login` later...
		IO.println(login.get());
	}

}

Properties

Lazy constants:

  • are easy to use

  • are truly final
    ⇝ allow constant folding

Best of both worlds.

Behavior

Details:

  • initializer executes at most once

  • no cancellation/timeout

  • equals compares LazyConstant identity

Error cases:

  • rejects null and cycles

  • if initializer fails, each get throws

All errors surface as
NoSuchElementException (with cause).

Lazy collections

var list = List.ofLazy(2, index -> "" + index);
var map = Map.ofLazy(Set.of(0, 1), key -> "" + key);
var set = Set.ofLazy(Set.of(0, 1), this::isInSet);

Lazy collections

Details:

  • initializer executes at most once per element

  • collections are unmodifiable

  • equals evaluates all elements
    (Collection requires this)

Error cases like before.

More

Third preview in JDK 27.

Java 27

…​
Java 27
Compact Object Headers
Lazy Constants
Structured Concurrency
PEM Encoding
The Rest
…​

Unstructured concurrency

Common way to organize concurrency:

  • have long-running ExecutorService instances

  • submit new tasks wherever necessary

  • enqueue follow-up computations wherever convenient

  • wait for results wherever needed

Concurrency is "all over the place".

Unstructured concurrency

Makes it more difficult to:

  • express/identify relationships between threads

  • organize overarching result/error handling

  • analyze control flow

  • debug issues that span multiple threads

Structured programming

We have seen this before with GOTOs.

Structured programming was the solution:

  • prescribes single entry point
    and clearly defined exit points

  • influenced languages and runtimes

Structured concurrency

When the flow of execution splits into multiple concurrent flows, they rejoin in the same code block.

Term coined/refined by:

Structured concurrency

// implicitly short-circuits on error
try (var scope = StructuredTaskScope.open()) {
	// spawns new (virtual) threads
	Subtask<String> a = scope.fork(this::taskA);
	Subtask<String> b = scope.fork(this::taskB);

	// waits explicitly for success
	// (throws error if there was one)
	scope.join();

	return a.get() + b.get();
} catch (FailedException ex) {
	// TODO: handle task errors
} // waits until all tasks/threads complete

Properties

Threads are short-lived:

  • start when task begins

  • end on completion

⇝ Establishes parent-child/sibling relationships
and logical grouping of tasks/threads.

Benefits

Structured concurrency:

  • defines a scope for concurrency

  • simplifies control flow

  • simplifies grouped result/error handling

  • makes thread relationships visible
    (in thread dumps and soon🤞 debuggers)

Completion

Use Joiner to configure result/error handling:

  • how are results collected?

  • when are subtasks cancelled?

  • what does join return?

  • when does join throw?

Pass to StructuredTaskScope.open(Joiner).

Strong Typing

Joiner<T, R, EX> and StructuredTaskScope<T, R, EX>
have the same type parameters:

  • T: result type of all subtasks

  • R: return type of join

  • EX: exception type of join

Joiners

Existing joiners for heterogeneous results:

  • Joiner<T, Void, ExecutionException>
    awaitAllSuccessfulOrThrow():

    • cancels/throws on first error

    • default behavior of open()

  • Joiner<T, List<Subtask<T>>, RuntimeException>
    allUntil(Predicate<Subtask<T>>):

    • cancels when the predicate is true

Joiners

Existing joiners for homogeneous results:

  • Joiner<T, List<T>, ExecutionException>
    allSuccessfulOrThrow():

    • cancels/throws on first error

  • Joiner<T, T, ExecutionException>
    anySuccessfulOrThrow()

    • returns first result

    • cancels/throws if all fail

Any Successful

try (StructuredTaskScope<String, String, Ex.Ex.> scope =
	StructuredTaskScope.open(
		Joiner.anySuccessfulOrThrow())) {
	// no need to grab the `Subtask` instances
	scope.fork(this::taskA);
	scope.fork(this::taskB);

	// returns the first successful result
	return scope.join();
} catch (ExecutionException ex) {
	// TODO: handle task errors
}

Custom exceptions

Most Joiner factories accept a function
to transform the exception:

// let the method throw `IOException`
try (StructuredTaskScope<String, String, IOEx> scope =
	StructuredTaskScope.open(
		Joiner.anySuccessfulOrThrow(IOException::new))) {

	scope.fork(this::taskA);
	scope.fork(this::taskB);

	return scope.join();
}

Custom joiners

Implement and pass to StructuredTaskScope::open:

interface Joiner<T, R, EX extends Throwable> {

	boolean onFork(Subtask<T> subtask);

	boolean onComplete(Subtask<T> subtask);

	R result() throws EX;

	R timeout() throws EX;

}

Configuration

Further configuration options:

  • the scope’s name

  • a timeout

  • a custom thread factory

Pass UnaryOperator<Configuration> to StructuredTaskScope::open.

Timeouts

try (StructuredTaskScope<String, String, Ex.Ex.> scope =
	StructuredTaskScope.open(
		Joiner.anySuccessfulOrThrow(IOException::new),
		cfg -> cfg.withTimeout(Duration.ofSeconds(1)))) {
	scope.fork(this::taskA);
	scope.fork(this::taskB);

	// throws when timeout expires
	return scope.join();
} catch (ExecutionException ex) {
	if (ex.getCause() instanceof
			CancelledByTimeoutException)
		// TODO: handle timeout
	else
		// TODO: handle task errors
}

Error propagation

If joiner cancels scope:

  • all child threads get interrupted

  • join returns or throws

But StructuredTaskScope.close() waits
until all child threads complete.

⇝ Handle InterruptedException properly!

More

Java 27

…​
Java 27
Compact Object Headers
Lazy Constants
Structured Concurrency
PEM Encoding
The Rest
…​

PEM texts

Representations of cryptographic objects
(keys, certificates, certificate revocation lists):

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj
0DAQcDQgAEi/kRGOL7wCPTN4KJ
2ppeSt5UYB6ucPjjuKDtFTXbgu
OIFDdZ65O/8HTUqS/sVzRF+dg7
H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----

Privacy-enhanced…​ mail?!

Was introduced for exchange via email
but moved way beyond:

  • development platforms (e.g. GitHub)

  • certificate authorities

  • cryptographic libraries (e.g. OpenSSL)

  • security-sensitive applications (e.g. OpenSSL)

  • hardware authentication devices (e.g. YubiKeys)

  • your applications

⇝ Very helpful if Java handled them.

Manual en-/decoding

To encode:

  • create binary representation of object

  • Base64-encode it

  • surround string with BEGIN/END lines

To decode: reverse that.

⇝ Java has all the building blocks, but
combining them is cumbersome.

PEM API

New PEM API is straightforward to use:

X509Certificate cert = // ...

// encode
PEMEncoder encoder = PEMEncoder.of();
String pem = encoder.encodeToString(cert);

// decode
PEMDecoder decoder = PEMDecoder.of();
BinaryEncodable cert2 = decoder.decode(pem);

assert cert.equals(cert2);

Endcoder/Decoder

Instances of PEMEncoder and PEMDecoder are:

  • immutable

  • thread-safe

  • reusable

Encodable

All implementations of BinaryEncodable can be encoded:

  • AsymmetricKey
    (DH, DSA, EC, RSA, etc.)

  • KeyPair

  • PKCS8EncodedKeySpec

  • X509EncodedKeySpec

  • X509Certificate

  • X509CRL

  • EncryptedPrivateKeyInfo

  • PEM

Decoding

For decoding:

  • if the object type is known, call
    decode(String, Class<T extends BinaryEncodable>):

    KeyPair kp = decoder.decode(pem, KeyPair.class);
  • otherwise, switch/instanceof over
    return value of decode(String)

(Overloads for InputStream exist.)

Unknown objects

Unknown cryptographic objects are decoded to PEM:

  • String type() (header text)

  • String content() (Base64-encoded body)

  • byte[] leadingData() (data preceding the header)

Encryption

PrivateKey, KeyPair, PKCS8EncodedKeySpec
instances can be encrypted/decrypted:

PrivateKey key = // ...
char[] pass = // ...

String pem = encoder
	.withEncryption(pass)
	.encodeToString(key);
BinaryEncodable decoded = decoder
	.withDecryption(pass)
	.decode(pem);

(Custom parameters/providers can be configured.)

More

Third preview in JDK 27.

Java 27

…​
Java 27
Compact Object Headers
Lazy Constants
Structured Concurrency
PEM Encoding
The Rest
…​

Rest of Java 27

Java 27

Java 22 to 25
Java 26
Java 27
Deprecations & Removals
Java 28

Cleaning house

Already removed:

Security Manager

What it was:

  • a system of checks and permissions

  • intended to safeguard security-relevant
    code sections

  • embodied by SecurityManager

Security Manager

What you need to know:

  • barely used but maintenance-intensive

  • disallowed by default since JDK 18

  • removed in JDK 24

    • command-line flags cause errors

    • SecurityManager still exists

    • calls are "no-ops" (where possible)
      or "no" (otherwise)

Security Manager

What you need to do:

  • observe your app with security manager disabled
    (java.security.manager=disallow on JDK 12+)

  • if used, move away from it

Cleaning house

Deprecated (for removal)

Finalization

What it is:

  • finalize() methods

  • a JLS/GC machinery for them

Finalization

What you need to know:

  • causes GC overhead and better alternatives exist

  • you can disable it with --finalization=disabled

  • in a future release, disabled will be the default

  • in a later release, finalization will be removed

Finalization

What you need to do:

  • search for finalize() in your code and
    replace with try-with-resources or Cleaner API

  • search for finalize() in your dependencies and
    help remove them

  • run your app with --finalization=disabled and
    closely monitor resource behavior (e.g. file handles)

Integrity by Default

Deprecated (for removal):

  • memory access via Unsafe ㉔ (JEP 498)

Disallow by default:

Unsafe Memory Access

What it is:

  • methods on sun.misc.Unsafe

  • allow direct memory access

Unsafe Memory Access

What you need to know:

  • superseeded by
    VarHandles (JEP 193) and FFM (JEP 454)

  • will be phased out:

    1. deprecation and compile-time warning ㉓

    2. run-time warning ㉔

    3. exception on invocation

    4. methods removed

Unsafe Memory Access

What you need to do:

  • find with --sun-misc-unsafe-memory-access:

    • warn: issue run-time warnings ㉔

    • debug: same with more info

    • deny: throw exceptions

  • report and help fix!

Final Field Mutation

What it is:

  • parts of the reflecion API (⇝ setAccessible)

  • can change values of most final fields

Final Field Mutation

What you need to know:

  • causes issues for:

    • correctness (any code can change any field)

    • performance (e.g. constant folding)

  • remains intact but app owner should opt in

  • Java 26 issues warnings on final field muation,
    except where explicitly enabled

  • a future version will disallow it where not enabled

Final Field Mutation

What you need to do:

  • use --enable-final-field-mutation to allow
    specified modules to mutate final fields

  • use --illegal-final-field-mutation for other code

Final Field Mutation

Three options for illegal mutation:

  • allow: allow without warning

  • warn: issue run-time warnings ㉖

  • debug: same with more info

  • deny: throw exceptions

In a future release, deny will become the only mode.

Prepare now by enabling mutation where necessary
plus --illegal-final-field-mutation=deny.

JNI

What it is:

  • machinery that allows Java programs
    to interact with native libraries

JNI

What you need to know:

  • native code can undermine Java’s integrity

  • remains intact but app owner should opt in

JNI

What you need to do:

  • use --enable-native-access to allow
    access to restricted JNI/FFM methods

  • use --illegal-native-access for other code

JNI

Three options for illegal native access:

  • allow: allow without warning

  • warn: issue run-time warnings ㉔

  • deny: throw exceptions

In a future release, deny will become the only mode.

Prepare now by enabling native access where necessary
plus --illegal-native-access=deny.

Agents

What it is:

  • a component that transforms byte code

  • uses java.lang.instrument or JVM TI

  • launches with JVM or attaches later ("dynamic")

Dynamic agents

What you need to know:

  • remains intact but app owner should opt in

  • nothing changed yet

  • in a future release, dynamic attach will be
    disabled by default

  • enable with -XX:+EnableDynamicAgentLoading

Dynamic agents

What you need to do:

  • run your app with -XX:-EnableDynamicAgentLoading

  • observe closely

  • investigate necessity of dynamic agents

More

Java 27

Java 22 to 25
Java 26
Java 27
Deprecations & Removals
Java 28

Java 28 in a Nutshell

Finalization:

Final features:

Java 28 in a Nutshell

New previews/incubations:

Java 22-27

Major achievements:

  • pave the on-ramp

  • add essential lower-level APIs

  • improve tooling and performance

  • introduce AOT workflow

Get JDK 27 at jdk.java.net/27.

So long…​

37% off with
code fccparlog

bit.ly/the-jms

More

Slides at slides.nipafx.dev
⇜ Get my book!

Follow Nicolai

nipafx.dev
🦋 🐘 /nipafx

Follow Java

inside.java // dev.java
/java    //    /openjdk

Image Credits