﻿# What's new in Java 27

Java 27 is here, with four targeted updates and five previews/incubators\. We take a closer look at the garbage collector changes, JFR improvements, post\-quantum TLS 1\.3, and compact object headers \(which cut memory usage\)\.

![1412_java27/image1.png](https://import.viva64.com/docx/blog/1412_java27/image1.png)

## Java platform changes

### JEP 534: Compact Object Headers by Default

[Link to JEP](https://openjdk.org/jeps/534)

In the [article about Java 25,](https://pvs-studio.com/en/blog/posts/java/1284/) we already mentioned compact object headers that we could turn on with a flag\. Now there's no need to use the flag\.

At that time, we only touched on this topic briefly\. Now that it's on by default, we're going to break it down once again, in more depth\.

Every object in the heap begins with a header\. In the classic layout \(in most JVMs\), that's two blocks:

* lock status, object's age for GC, identity hash;
* a compact pointer to the class's metadata\.

In total, we get 96 bits \(12 bytes\) per object, even if it contains very little useful data\. Compact object headers compress all of this into 64 bits: the pointer to the class metadata is compressed down to 22 bits\.

![1412_java27/image2.png](https://import.viva64.com/docx/blog/1412_java27/image2.png)

In theory, saving a few bits is a good thing, though, but what benefits shall we \(as JRE users\) actually get out of it? 

Not long ago, we [released](https://pvs-studio.com/en/blog/posts/1406/) a JavaScript/TypeScript analyzer powered by Java 25\. We'll use it as a test subject, "set it loose" on the [Grafana source code](https://github.com/grafana/grafana), and run it with and without compact object headers\.

Three minutes later, we get this:

|\*\*Benchmark\*\*|\*\*With flag\*\*|\*\*Without flag\*\*|
|---|---|---|
|Time|81\\\.4 sec|79\\\.4 sec|
|Committed heap|1 290 240K \\\(\\\~1\\\.23 GB\\\)|1 126 400K \\\(\\\~1\\\.07 GB\\\)|
|Used heap: output|1 150 092K \\\(\\\~1\\\.1 GB\\\)|773 062K \\\(\\\~738 MB\\\)|

Committed heap represents the amount of memory the virtual machine has allocated from the OS and is keeping reserved\. Used heap is the memory in use right now—in other words, the number of bytes occupied by live objects that haven't yet been removed by the garbage collector\.

As a result, analysis time remained almost unchanged \(within margin of error\), committed heap decreased by 13%, and used heap shrank by 33% in total\! Just one flag reduced our app's memory usage by a full third, wow\!

What's the cost of our joy? A 22\-bit pointer limits the number of loaded classes to approximately four million\. There's no automatic rollback to a non\-compact mode—if the limit is exceeded, we'll get an `OutOfMemoryError`\. If your app really hits this limit, you must really love long\-lived streams\. But if you want to revert to the previous mode, you can do it with another flag:

```cpp
java -XX:-UseCompactObjectHeaders Application
```

### JEP 523: Make G1 the Default Garbage Collector in All Environments

[Link to JEP](https://openjdk.org/jeps/523)

Prior to Java 27, the JVM would check the machine at startup, and if it detected fewer than two processors or less than 1,792 MB of memory, it'd just use Serial GC instead of G1\. That kind of machine got labeled a "client" machine, and the memory allocator best suited for it was used\.

Now, guess what a modest JVM container with a limit of one CPU and 512 MB of memory looks like?

Right\. A chunk of containerized Java apps has been running on a client\-oriented collector all this time, even though no one consciously made that decision\. Worse yet, the same app could get different GCs on the server and in the developer's IDE, which turned reproducing pausing issues into a thrilling quest with a touch of manic laughter\.

Java 27 stopped taking the environment into account and now G1 is selected by default:

```cpp
# Java 25
$ java -XX:ActiveProcessorCount=1 -Xmx256m -Xlog:gc --version
[0.001s][info][gc] Using Serial
 
# Java 27
$ java -XX:ActiveProcessorCount=1 -Xmx256m -Xlog:gc --version
[0.001s][info][gc] Using G1
```

The reasoning is simple: improvements to G1's throughput in previous releases have reduced the gap with Serial to almost zero, and G1's pauses are noticeably more pleasant because it can operate concurrently and in parallel\. Plus, predictability: the application's behavior no longer depends on how many cores the JVM detected at startup\.

If you specifically want to use `Serial`—it still has the lowest memory footprint and faster startup time—you can still use it: `-XX:+UseSerialGC`\.

## Security

### JEP 527: Post\-Quantum Hybrid Key Exchange for TLS 1\.3

[Link to JEP](https://openjdk.org/jeps/527)

Ever heard of "harvest now, decrypt later"?

This is an amusing category of threats, though: attackers, having access to your traffic but unable to break the TLS connection, simply save the encrypted traffic for later\.

Why would they do that, what's the purpose? It's very curious: attackers have a strong faith in global progress and are waiting for a quantum computer capable of breaking elliptic curve key exchange\. Medical and banking records, encryption keys, personal data—all of these will still be valuable ten years from now, so the scheme is quite viable\.

Following HTTP/3 \(see the [previous article](https://pvs-studio.com/en/blog/posts/java/1370/#ID6A3D58AEB3)\), support for TLS 1\.3 is now available\. The session key in TLS 1\.3 now gets generated using two algorithms simultaneously: a classic elliptic curve key exchange plus a quantum\-resistant algorithm\. Such a pairing is called _a hybrid group_, and the point is that the connection remains secure as long as at least one of the two algorithms is still in service\. So this update makes it pointless to wait for a quantum computer to arrive\.

For the application, this is free in terms of code: `javax.net.ssl` automatically sets the hybrid group as the first in the preference list\. If the other side supports it, the exchange gets a hybrid approach; if not, everything works as before\. There's nothing to touch, unless you've manually set a list of groups via `jdk.tls.namedGroups` or `SSLParameters::setNamedGroups`\.

But in some cases, we'll still have to pay a little extra: a TLS handshake costs about two KB more\. For most applications, this goes unnoticed, but with millions of short connections in the traffic, you'll see it\.

An important note: this JEP applies only to key exchange\. Certificate signatures are still in the classic format, and this is a deliberate choice: it is impossible to forge a signature retroactively, but it is entirely possible to decrypt recorded traffic\.

## JFR enhancements

### JEP 536: JFR In\-Process Data Redaction

[Link to JEP](https://openjdk.org/jeps/536)

When recording, [JFR](https://openjdk.org/jeps/328) carefully preserves the entire process environment: environment variables, system properties, and command\-line arguments\. And that's where, as we all know, database passwords, tokens, and keys to everything tend to live\. That's why sending a `.jfr` file to a colleague—and especially attaching it to a public issue—always came with a slight twinge of anxiety\. In most cases, you shouldn't make `.jfr` files publicly available at all without cleaning them up first\.

Starting with Java 27, JFR scrubs sensitive values within the JVM before they're passed to disk\. And what does JFR consider to be sensitive data? By default, any environment variable or system property whose name matches one of these patterns gets replaced with `[REDACTED]`:

```cpp
*api*key*      *auth*        *client*secret*  *credential*
*jaas*config*  *passphrase*  *passwd*         *password*
*private*key*  *pwd*         *secret*         *token*
```

Command\-line arguments of the form `--db-password qwerty123` are handled in the same way\.

We can expand the list of properties using the `-XX:FlightRecorderOptions:redact-key=+<your-pattern>` flag\.

It's worth understanding the limits here: the mechanism covers start events \(environment variables, system properties, arguments, and JVM information\) and is explicitly described as best\-effort\. It doesn't filter your app's events, exception messages, or child process command lines\. In other words, `.jfr` has become significantly safer now, but not sterile in total\. We're still worried, but not as much as before\.

## In preview

A few more JEPs have been included in the release as previews\. We won't cover them for now, we'll wait for their final versions\. Here are some links for you to explore on your own:

* [JEP 531: Lazy Constants \(Third Preview\)](https://openjdk.org/jeps/531)
* [JEP 532: Primitive Types in Patterns, instanceof, and switch \(Fifth Preview\)](https://openjdk.org/jeps/532)
* [JEP 533: Structured Concurrency \(Seventh Preview\)](https://openjdk.org/jeps/533)
* [JEP 537: Vector API \(Twelfth Incubator\)](https://openjdk.org/jeps/537)
* [JEP 538: PEM Encodings of Cryptographic Objects \(Third Preview\)](https://openjdk.org/jeps/538)

## Conclusion

The full list of JEPs is [here](https://openjdk.org/projects/jdk/27/)\.

This was a minor release, and there was nothing here in the way of earth\-shattering, 180\-degree changes: they've updated the preview and polished up the platform\. And that brings useful thing right out of the box for the JVM: objects on the heap are more compact, the garbage collector is the same for both the server and a tiny pod, JFR logs are no longer brimming with secrets, and TLS is preparing for a quantum future\.

Essentially, this is preparation for the next LTS\. [Structured Concurrency](https://openjdk.org/jeps/533) is now on its seventh preview, [primitives in patterns](https://openjdk.org/jeps/532) are in their fifth preview, and the [Vector API](https://openjdk.org/jeps/537) is waiting for [Valhalla](https://openjdk.org/projects/valhalla/)\. All of this is being finalized so that it can be delivered ready to go with Java 29\. Well, for now, though, let's just update and get a more compact heap for free\.