Die kontinuierliche Weiterentwicklung von Java zeigt sich in der Vielzahl von Neuerungen, die sowohl die Sprache als auch die Plattform und das Laufzeitsystem betreffen. Mit diesem Release setzt sich der Trend fort, Java durch gezielte Verbesserungen moderner, effizienter und leistungsfähiger zu machen. Neben Änderungen an der Sprache gibt es auch Optimierungen in der JVM, neue APIs sowie diverse produktivitätssteigernde Fortschritte für Entwicklerinnen und Entwickler.
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Bei genauerer Betrachtung fallen zunächst viele Themen ins Auge, die bereits in früheren Versionen enthalten waren. Einige der Wiedervorlagen stehen mit Java 25 nun in einer finalen Fassung bereit. Dazu zählen die JDK Enhancement Proposals (JEPs) 506 (Scoped Values), 511 (Module Import Declarations), 512 (Compact Source Files and Instance Main Methods) und 513 (Flexible Constructor Bodies). Insgesamt wurden die folgenden 18 JEPs umgesetzt [1]:
- 470: PEM Encodings of Cryptographic Objects (Preview)
- 502: Stable Values (Preview)
- 503: Remove the 32-bit x86 Port
- 505: Structured Concurrency (Fifth Preview)
- 506: Scoped Values
- 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
- 508: Vector API (Tenth Incubator)
- 509: JFR CPU-Time Profiling (Experimental)
- 510: Key Derivation Function API
- 511: Module Import Declarations
- 512: Compact Source Files and Instance Main Methods
- 513: Flexible Constructor Bodies
- 514: Ahead-of-Time Command-Line Ergonomics
- 515: Ahead-of-Time Method Profiling
- 518: JFR Cooperative Sampling
- 519: Compact Object Headers
- 520: JFR Method Timing & Tracing
- 521: Generational Shenandoah
Starten wir zunächst mit dem Langläufer der letzten Jahre, dem Vector API. Es ist nun schon das zehnte Mal als Inkubator enthalten und taucht seit Java 16 regelmäßig in den Releases auf. Es geht dabei um die Unterstützung der modernen Möglichkeiten von SIMD-Rechnerarchitekturen mit Vektorprozessoren. Single Instruction, Multiple Data (SIMD) lässt viele Prozessoren gleichzeitig unterschiedliche Daten verarbeiten. Durch die Parallelisierung auf Hardwareebene verringert sich beim SIMD-Prinzip der Aufwand für rechenintensive Schleifen.
Der Grund für die lange Inkubationsphase des Vector API wird in den Zielen des JEP 508 [2] erklärt: „Alignment with Project Valhalla – The long-term goal of the Vector API is to leverage Project Valhalla’s enhancements to the Java object model. Primarily this will mean changing the Vector API’s current value-based classes to be value classes so that programs can work with value objects, i.e., class instances that lack object identity.“
Man wartet also auf die Reformen am Typsystem. Bei Java ist es aktuell zweigeteilt in primitive und Referenztypen (Klassen). Die primitiven Datentypen wurden ursprünglich aus Gründen der Performanceoptimierung eingeführt, haben aber im Handling entscheidende Nachteile. Referenztypen sind auch nicht immer die beste Wahl, insbesondere was die Effizienz und den Speicherverbrauch angeht. Es braucht etwas dazwischen, was sich so schlank und performant wie primitive Datentypen verhält, aber auch die Vorzüge von selbst zu erstellenden Referenztypen in Form von Klassen kennt. Schon bald könnten daher aus dem Inkubatorprojekt Valhalla die Value Types (haben keine Identität) und Universal Generics (List<int>) ins JDK übernommen werden. Das JEP 401 (Value Classes and Objects) hat es leider wieder nicht geschafft, aber vielleicht sehen wir es ja dann im OpenJDK 26. Dementsprechend werden wir das Vector API wohl auch noch einige Releases lang als Inkubator- bzw. dann hoffentlich bald als Preview-Feature wiedersehen.
Deferred Immutability
Das JEP 502 führt mit „Stable Values“ Objekte ein, die genau einmal gesetzt werden und danach unveränderlich sind. Die JVM behandelt ihren Inhalt wie eine echte Konstante und kann dieselben Optimierungen (z. B. Constant Folding) anwenden wie bei final‑Feldern, sie bieten aber eine höhere Flexibilität in Bezug auf den Initialisierungszeitpunkt. Damit bekommt Java eine „Deferred Immutability“, die Start‑ und Aufwärmzeiten verkürzt, ohne Thread‑Sicherheits-Risiken einzugehen.
Die Ziele sind ein schnellerer Start (keine monolithische Initialisierung aller Komponenten mehr), Entkopplung von Erzeugung und Initialisierung eines unveränderlichen Werts ohne nennenswerte Performanceeinbußen, garantierte At‑most‑once‑Initialisierung auch in hoch parallelem Code sowie das Zugänglichmachen der Konstantenoptimierungen für Anwendungscode (analog zu JDK‑internem Code).
Stable Values sind ein reines Library API, es wird kein neues Schlüsselwort geben. Außerdem gibt es keine Änderung an der final‑Semantik, bestehender Code bleibt unberührt. Bisher müssen final‑Felder eager (sofort) initialisiert werden – die Initialisierung eines Loggers oder eine Datenbank‑Connection bremst so den Programmstart aus. Workarounds wie Lazy Holder, Double‑Checked Locking oder ConcurrentHashMap.computeIfAbsent sind entweder eingeschränkt, fehleranfällig oder verhindern JIT‑Optimierungen (Just in Time). Stable Values schließen die Lücke zwischen strenger Immutability und flexibler Lazy Initialization.
In Listing 1 wird der Lambdaausdruck in orElseSet garantiert genau einmal ausgeführt, selbst wenn mehrere Virtual Threads gleichzeitig logger() aufrufen. Danach kann der JIT alle Zugriffe optimieren (z. B. mit Constant Folding).
class OrderController { private final StableValue<Logger> logger = StableValue.of(); private Logger logger() { return logger.orElseSet(() -> Logger.create(OrderController.class)); } }
Das API von Stable Values zeigt Tabelle 1. Stable Values kombinieren Lazy und Fast Initialization. Risiken bestehen praktisch nur dort, wo Reflection weiterhin final-Felder ändern darf. Aber das ist bereits heute eine generelle JVM-Beschränkung, und Stable Values stellen somit eine sichere Variante dar, diese Beschränkung zu umgehen. Sie passen hervorragend zu Virtual Threads (Loom) und Structured Concurrency (JEP 505). Theoretisch können nun Millionen Threads ohne teure Synchronisationstricks auf „Deferred Constants“ zugreifen.
Methode | Zweck | Thread-sicher? | JIT-optimierbar? |
---|---|---|---|
StableValue.of() | leere Hülle erzeugen | ✔ | ✔ |
orElseSet(Supplier) | Inhalt holen oder einmalig setzen | ✔ | ✔ |
StableValue.supplier(Supplier) | kombiniert StableValue + Supplier | ✔ | ✔ |
StableValue.list(int, IntFunction) | Pool/Liste lazily erzeugen | ✔ | ✔ |
Tabelle 1: API von Stable Values
Primitive Type Patterns
Pattern Matching ist nun auch schon einige Jahre in der Entwicklung. Hier wurden immer wieder Teile abgeschlossen, zuletzt in Java 22 die „Unnamed Variables & Patterns“ (JEP 456). Bei den nun in der dritten Preview befindlichen „Primitive Types in Patterns, instanceof, and switch“ (JEP 507) geht es um eine Erweiterung, sodass primitive Datentypen wie int, byte und double in allen Pattern-Kontexten (beim instanceof und im switch) verwendet werden dürfen. Entwickler haben dadurch weniger Limitierungen sowie Sonderfälle und können primitive und Referenzdatentypen auch im Kontext von Type Patterns oder als Komponenten in Record Patterns austauschbar verwenden. Seit der letzten Preview gibt es keine Änderungen, die JDK-Entwickler wollen aber weiteres Feedback sammeln, wie es heißt.
Beim Pattern Matching geht es darum, bestehende Strukturen mit Mustern abzugleichen, um komplizierte Fallunterscheidungen effizient und wartbar implementieren zu können. Ein Pattern ist dabei eine Kombination aus einem Prädikat (das auf die Zielstruktur passt) und einer Menge von Variablen innerhalb dieses Musters. Diesen Variablen werden bei passenden Treffern die entsprechenden Inhalte zugewiesen und damit extrahiert. Die Intention des Pattern Matching ist die Destrukturierung von Datenobjekten, also das Aufspalten in die Bestandteile und das Zuweisen in einzelne Variablen zur weiteren Bearbeitung. Mit instanceof und switch können wir also überprüfen, ob ein Objekt von einem bestimmten Typ ist, und wenn ja, dieses Objekt einer Variable dieses Typs zuweisen und diese Variable in dem folgenden Programmpfad benutzen. Das funktionierte bisher nur mit Objekten und ließ sich nicht mit primitiven Datentypen kombinieren. Einzig im switch ließen sich bereits Variablen der primitiven Typen byte, short, char sowie int gegen Konstanten matchen und konnten sogar mit den neueren Type Patterns kombiniert werden (Listing 2).
int grade = 7; String result = switch (grade) { case 1, 2 -> "very good or good"; case 3, 4 -> "satisfactory or sufficient"; case 5, 6 -> "poor or deficient"; case Integer i -> "Undefined grade: " + i; }; System.out.println(result);
JEP 507 verbessert nun Typprüfung, Performance und Lesbarkeit in Java und macht Pattern Matching konsistenter, indem es primitive Typen direkt unterstützt. Das reduziert überflüssiges Autoboxing und vereinfacht den Umgang mit primitiven Datentypen in switch-Anweisungen. Wie am Beispiel in Listing 3 zu sehen ist, können Entwickler in Zukunft ganz einfach prüfen, ob ein ganzzahliger Wert in den Wertebereich eines bytes passt.
private static String checkByte(int value) { if (value instanceof byte b) { return "byte b = " + b; } else { return "kein byte: " + value; } } System.out.println(checkByte(127)); // b = 127 System.out.println(checkByte(128)); // kein byte: 128
Leichtere Entwicklung mit Java – für Einsteiger und Profis
Trotz seiner 30 Jahre zieht Java auch weiterhin viele Programmieranfänger an. Die JEPs 511 (Module Import Declarations) und 512 (Compact Source Files and Instance Main Methods) helfen aber nicht nur Neulingen, sie erleichtern auch erfahrenen Entwicklern das Leben. Beide JEPs werden nun nach mehreren vorangegangenen Previews finalisiert.
Durch die „Module Import Declarations“ lassen sich alle exportierten Packages eines Moduls auf einmal importieren. Das reduziert Import-Boilerplate und vereinfacht die Wiederverwendung modularer Bibliotheken (ohne dass der eigene Code selbst modularisiert sein muss). Das ist praktisch fürs Prototyping und für klare, kurze Quelltexte. In der Finalisierung hat sich nichts mehr geändert, aber bei der letzten Preview gab es zwei Erweiterungen. Einerseits wurden Beschränkungen bei den transitiven Abhängigkeiten des Moduls java.se (eine Art Aggregator-Modul ohne eigene Packages/Klassen) zu java.base aufgehoben. Dadurch kann man nun mit dem Import dieses einen Moduls das gesamte API von Java SE importieren. Außerdem ist es jetzt möglich, dass sogenannte Type-Import-on-demand-Deklarationen (z. B. import java.util.*) vorherige Modul-Import-Deklarationen überdecken. Wenn beispielsweise die Module java.base und java.sql importiert werden, gibt es eine Unklarheit beim Verwenden der Klasse Date – es gibt sie als java.util.Date und als java.sql.Date. Durch die On-demand-Deklaration import java.util.* wird in dem Fall java.util.Date verwendet.
Zum JEP 512 (Compact Source Files and Instance Main Methods) gab es bereits vier Previews und der Name hat sich immer mal wieder geändert. Es werden kompakte Ein-Datei-Programme mit einer Instanz-main-Methode ermöglicht, sodass kleine Tools, Skripte oder Lernbeispiele ohne unnötige „Zeremonie“ startklar sind. Das führt zu einem schnelleren Einstieg für Anfänger und weniger Hürden für erfahrene Entwickler beim Skizzieren, Ausprobieren und Automatisieren. In der Finalisierung gab es noch einmal drei kleine Änderungen:
- Die Klasse IO liegt nun im Package java.lang und wird damit immer automatisch importiert.
- Die statischen Methoden von IO werden nicht mehr implizit in Compact Source Files importiert, Aufrufe müssen qualifiziert (z. B. IO.println(“Hello, world!”)) oder explizit importiert werden.
- Die Implementierung von IO stützt sich jetzt auf System.out/System.in statt auf java.io.Console.
Unter Beibehaltung der bestehenden Java Toolchain und nicht mit der Absicht, einen separaten Dialekt für Java einzuführen, lassen sich in Compact Source Files mit der vereinfachten main-Methode (Klassendeklaration entfällt …) sehr simple, skriptartige Programme erstellen. Dazu kommt die leichtere Verwendung von Standardein- und -ausgabe durch die neuen statischen Methoden print(), println() und readln() der Klasse java.io.IO. Ein Beispiel zeigt der folgende Code:
// > java Main.java
void main() {
IO.println("Hello, World!");
}
Neues aus dem Umfeld von Virtual Threads
Für die in Java 21 finalisierten virtuellen Threads wurden zwei weitere APIs eingeführt: „Structured Concurrency“ und „Scoped Values“. Sie ermöglichen eine effizientere, sichere und besser strukturierte Nebenläufigkeit.
Scoped Values werden nun mit den JEP 506 finalisiert. Ein ScopedValue<T> ist eine Alternative zu ThreadLocal<T>, die speziell für Virtual Threads optimiert wurde. Er ermöglicht es, unveränderbare Werte sicher über einen Codebereich hinweg zu propagieren, ohne dabei die Nachteile von ThreadLocal zu übernehmen. Diese sind bei Virtual Threads problematisch, da Speicherplatz potenziell nicht automatisch freigegeben wird. Da Virtual Threads sehr leichtgewichtig sind und potenziell tausende parallele Aufgaben ausgeführt werden können, würden ThreadLocal-Variablen schlecht skalieren. Denn jeder Thread würde seinen eigenen Wert speichern. Bei Scoped Values sind die Inhalte nur innerhalb eines bestimmten Bereichs gültig und verursachen dadurch keine Speicherprobleme. Scoped Values sind sicherer, performanter und expliziter in ihrer Lebensdauer. Durch ihre Unveränderbarkeit können keine ungewollten Nebenwirkungen auftreten. Listing 4 zeigt ein Beispiel, bei dem USER_ID nur innerhalb des run()-Blocks gültig ist und dann automatisch aufgeräumt wird.
public class ScopedValueExample { private static final ScopedValue<String> USER_ID = ScopedValue.newInstance(); public static void main(String[] args) { ScopedValue.where(USER_ID, "User-123").run(() -> { processRequest(); }); } static void processRequest() { System.out.println("Processing for user: " + USER_ID.get()); // Gibt "User-123" aus } }
Im Vergleich zur letzten Preview gibt es in JEP 506 nur noch eine kleine Änderung: Die Methode ScopedValue.orElse() akzeptiert keine null-Argumente mehr.
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Die Structured Concurrency (JEP 505) sorgt dafür, dass nebenläufige Tasks innerhalb eines klar definierten Scopes gestartet und beendet werden. Das hilft bei der Fehlerbehandlung (wenn eine Aufgabe fehlschlägt, werden alle anderen koordiniert abgebrochen) und bei der Lesbarkeit bzw. Wartbarkeit (explizite Nebenläufigkeitsstrukturen werden sichtbar). Alternativ konnten Entwickler für diesen Zweck bisher die Parallel Streams, den ExecutorService oder reaktive Programmierung einsetzen. Alles sehr mächtige Ansätze, die aber einfache Umsetzungen unnötig kompliziert und fehleranfällig machen. Structured Concurrency behandelt Gruppen zusammengehöriger Aufgaben als eine Arbeitseinheit, wodurch die Fehlerbehandlung sowie das Abbrechen der Aufgaben vereinfacht und die Zuverlässigkeit sowie die Beobachtbarkeit erhöht werden.
Listing 5 zeigt ein Beispiel. Beide Subtasks laufen parallel (typisch als Virtual Threads) los. Wenn einer fehlschlägt, bricht der Scope ab und unterbricht den anderen Task automatisch, join() wirft dann eine FailedException. Waren beide erfolgreich, sind nach join() beide Ergebnisse verfügbar.
// --enable-preview beim Kompilieren und Starten record Response(String user, int order) {} Response handle() throws InterruptedException { try (var scope = StructuredTaskScope.<Object, Void>open()) { var user = scope.fork(this::findUser); // liefert String var order = scope.fork(this::fetchOrder); // liefert Integer scope.join(); // wartet auf alle; wirft eine Exception bei einem Fehler und cancelt die anderen return new Response((String) user.get(), (Integer) order.get()); } } // Platzhalter: String findUser() { return "alice"; } int fetchOrder() { return 42; }
Gegenüber der letzten Preview hat sich noch einmal einiges an dem API verändert:
- Statische Fabrikmethoden zum Öffnen eines Scopes (StructuredTaskScope.open(…)) statt öffentlicher Konstruktoren
- Die Standardvariante ist open() ohne Parameter: Der Scope gilt als erfolgreich, wenn alle Subtasks erfolgreich sind; schlägt einer fehl, wird der Scope abgebrochen (Short Circuit)
- Joiner-Konzept: Über open(Joiner…) definierst du alternative Abschluss-Policies (z. B. „erstes erfolgreiches Ergebnis“ oder „sammle alle Ergebnisse“), und join() liefert dann direkt das passende Ergebnis
- join() wirft jetzt selbst Exceptions (z. B. FailedException, TimeoutException) und liefert ggf. einen Rückgabewert; das frühere Pattern join().throwIfFailed() entfällt
- Es gibt eine optionale Konfiguration beim Öffnen (open(joiner, cfg -> cfg.withTimeout(…).withThreadFactory(…))) für Timeouts, ThreadFactory und Scope-Name – hilfreich für Monitoring und Tuning
- Observability wurde erweitert: Thread-Dumps (JSON) zeigen die Task-/Subtask-Bäume inkl. Scope-Beziehungen
Listing 6 zeigt einige Beispiele für alternative TaskScope-Strategien.
// Erstes erfolgreiches Ergebnis gewinnt StructuredTaskScope.open( Joiner.<T>anySuccessfulResultOrThrow(), cfg -> cfg.withTimeout(java.time.Duration.ofSeconds(2))) // Alle oder keiner StructuredTaskScope.open(Joiner.<T>allSuccessfulOrThrow()) // Erfolgreiche und fehlgeschlagene Tasks unterscheiden StructuredTaskScope.open(Joiner.<T>awaitAll()) // Früher Abbruch per Prädikat StructuredTaskScope.open( Joiner.<String>allUntil(st -> st.state() == Subtask.State.SUCCESS && st.get().contains("OK")))
Flexible Konstruktorinhalte werden finalisiert
Dank des JEP 513 (Flexible Constructor Bodies) dürfen in Konstruktoren Anweisungen nun bereits vor einem expliziten weiteren Konstruktoraufruf (super() oder this()) im sogenannten Prolog erscheinen. Die Anweisungen im Prolog unterliegen einigen Einschränkungen, es ist insbesondere keine Verwendung von this erlaubt. Dazu zählen weder das Lesen von Feldern noch das Aufrufen von Methoden. Nicht erlaubt sind z. B. this.hashCode(), System.out.println(this), var x = this.feld (lesender Zugriff). Es gibt genau eine Ausnahme, einfache Zuweisungen auf eigene Felder ohne Initializer sind erlaubt (z. B. this.feld = …). Diese Zuweisungen dürfen aber nicht innerhalb von Lambdas oder inneren Klassen im Prolog stehen.
Im Prolog können jedoch Parameter validiert bzw. transformiert werden. Außerdem kann im Konstruktor der Oberklasse auf bereits initialisierte Felder der Subklasse zugegriffen werden. Listing 7 zeigt ein solches Beispiel, in dem nicht nur Felder validiert oder transformiert werden (Name aufsplitten), sondern auch in der Superklasse bereits auf die Werte von Feldern der Subklasse zugegriffen wird, obwohl der Subklassenkonstruktor noch nicht beendet ist (Aufruf von show() im Konstruktor Person).
void main() { new Employee("Duke Java", 42, "C-7"); } class Person { int age; String firstname, lastname; void show() { IO.println(this); } Person(String name, int age) { // Prolog String[] names = name.split("\\s" ); this.firstname = names[0]; this.lastname = names[1]; if (age < 0) { // fail-fast im Prolog throw new IllegalArgumentException("age must be greater than or equal to zero"); } this.age = age; super(); // Epilog show(); // ruft ggf. überschriebenes show() in Subklasse auf! } @Override public String toString() { return String.format("Name=%s %s; Age=%d", firstname, lastname, age); } } class Employee extends Person { String officeId; // kein Initializer → im Prolog setzbar Employee(String name, int age, String officeId) { // Prolog this.officeId = Objects.requireNonNull(officeId); // Prolog: Feld der eigenen Klasse setzen super(name, age); // erst jetzt Superkonstruktor // Epilog: weiterer Code erlaubt, Objekt ist konsistent } @Override public String toString() { return super.toString() + "; OfficeId=" + officeId; } }
Das macht viele Konstruktoren natürlicher (z. B. Fail-fast-Validierung) und erhöht die Sicherheit, weil Felder eines Subtyps fertig initialisiert sein können, bevor Superklassenkonstruktoren (ggf. via überschreibbare Methoden) darauf zugreifen. Insgesamt erhöht sich sowohl die Lesbarkeit als auch die Performance, da unnötige Aufrufe bei der Validierung oder Weiterverarbeitung von Konstruktorparametern vermieden werden. Dieses Feature wird nun finalisiert, im Vergleich zur letzten Preview gab es keine Änderungen.
Was sonst noch geschah
Neben diesen prominenten und auch für viele Entwickler relevanten Neuerungen gibt es auch wieder einige kleine Änderungen. So wurden mit dem JEP 503 der Quellcode und Build-Support für den 32-Bit-x86-Port (nach Deprecation in JDK 24 via JEP 501) dauerhaft entfernt. Ziel ist es, neue Features nicht länger mit 32-Bit-Fallbacks bedienen zu müssen, alle 32-Bit-x86-Sonderpfade zu streichen und Build/Test-Infrastruktur zu vereinfachen. Nicht betroffen sind der 32-Bit-Support anderer Architekturen und frühere JDK-Releases. Der Pflegeaufwand stand in keinem Verhältnis zum Nutzen. Die Entfernung beschleunigt die Weiterentwicklung z. B. bei Loom, FFM API, Vector API, GC-Barrier-Expansion usw.
Schneller Start und Warm-up
Das JEP 514 (Ahead-of-Time Command-Line Ergonomics) wird das Erzeugen von Ahead-of-Time-Caches (AOT) vereinfachen. Diese können den Start von Java-Anwendungen deutlich beschleunigen. Für gängige Fälle soll jetzt ein einziger Schritt genügen, der Trainingslauf und Cacheerstellung kombiniert. Ziel ist es, die bislang nötige Zwei-Phasen-Prozedur abzulösen – ohne an Ausdrucksstärke zu verlieren und ohne neue AOT-Optimierungen einzuführen. Heute braucht man zwei java-Aufrufe und bleibt mit einer temporären *.aotconf-Datei zurück; künftig entfällt dieser Ballast, was u. a. Frameworks wie JRuby bei eigenen Trainingsläufen entgegenkommt. Für Spezialfälle bleiben die expliziten AOT-Modi und Konfigurationsoptionen aber weiterhin verfügbar.
Mit dem JEP 515 (Ahead-of-Time Method Profiling) wird es eine schnellere Warm-up-Phase durch Profile aus dem AOT-Cache geben. Die JVM kann beim Start Methodenausführungsprofile aus einem früheren Lauf sofort laden, sodass der JIT direkt die voraussichtlich relevanten Methoden kompiliert, statt zunächst Profile sammeln zu müssen. Die Profile werden in einem Trainings-Run erzeugt und über den bestehenden AOT-Cache bereitgestellt. Dafür sind keine Codeänderungen und keine neuen Workflows nötig.
Der Hintergrund ist, dass das Warm-up heute viel Zeit kostet, weil die HotSpot VM erst während der Produktion herausfindet, welche Methoden relevant sind. Durch das Verlagern des Profilings in einen Trainings-Run, erreicht die Anwendung in Produktion schneller ihre Spitzenleistung. Ein Web-Service, der sonst erst nach einigen Minuten voll performant ist, kann mit vorab aufgezeichneten Profilen schon beim Start die kritischen Request-Pfade kompilieren und so unter Last sofort schneller reagieren.
Verbesserungen bei Beobachtbarkeit und Profiling
Der JDK Flight Recorder wird mit dem experimentellen JEP 509 (JFR CPU-Time Profiling) so erweitert, dass er auf Linux den CPU-Zeit-Timer des Kernels nutzt und damit präzisere CPU-Profile erstellt – auch dann, wenn Java-Code gerade native Bibliotheken ausführt. Bisher stützte sich JFR (JDK Flight Recorder) vor allem auf einen „Execution Sampler“, der in festen Echtzeitintervallen (z. B. alle 20 ms) Java-Stacks zieht, dabei aber Threads in nativem Code übersieht, Proben verpassen kann und nur einen Teil der Threads erfasst. Mit CPU-Zeit-Profiling bildet JFR die tatsächlich verbrauchten CPU-Zyklen ab und vermeidet unsichere interne Schnittstellen, wie sie manche externe Tools verwenden. Ein Sortieralgorithmus verbringt zum Beispiel seine gesamte Laufzeit auf der CPU und taucht entsprechend stark im CPU-Profil auf, eine Methode, die meist auf Daten aus einem Socket wartet, beansprucht hingegen kaum CPU-Zeit und erscheint dadurch nur selten im Profil.
Durch das JEP 518 (JFR Cooperative Sampling) soll der JFR stabiler werden, indem er Java-Thread-Stacks nur noch an Safepoints traversiert und dabei den bekannten Safepoint-Bias so weit wie möglich reduziert. Bisher nahm der JFR in festen Intervallen (z. B. alle 20 ms) asynchron Samples auch außerhalb von Safepoints. Das erforderte Heuristiken zum Stack-Parsing, die ineffizient waren und im Fehlerfall sogar JVM-Crashes auslösen konnten (etwa bei gleichzeitigem Class Unloading). Künftig wird auch die Profilerstellung der Wanduhrzeit (Wall-Clock Time) weiter unterstützt, aber mit einem robusteren Verfahren, das Genauigkeit und Ausfallsicherheit besser ausbalanciert.
Das JEP 520 (JFR Method Timing & Tracing) erweitert den JDK Flight Recorder um eine Bytecodeinstrumentierung, die jeden ausgewählten Methodenaufruf exakt misst und mit Stacktrace aufzeichnet – im Gegensatz zu stichprobenbasierten Profilen. Methoden lassen sich ohne Codeänderungen zielgenau per Kommandozeile, Konfigurationsdatei, jcmd oder JMX auswählen. Nicht vorgesehen sind das Aufzeichnen von Argumenten/Feldwerten, das Tracen nicht bytecodierter Methoden (z. B. native, abstract) sowie das gleichzeitige Instrumentieren sehr vieler Methoden. In solchen Fällen bleibt Sampling der richtige Ansatz. Um zum Beispiel lange Startzeiten zu analysieren, können statische Initialisierer ausgewählter Klassen gezielt getracet werden. So wird sichtbar, welche Initialisierung sich auf später verschieben lässt oder wo ein jüngst eingespielter Fix tatsächlich Laufzeit spart.
Geringerer Speicherverbrauch und effizientere Garbage Collection
Mit dem JEP 519 werden die Compact Object Headers finalisiert. Sie werden jedoch nicht zum Standardlayout erklärt. Seit ihrer Einführung im JDK 24 (JEP 450) haben sie sich in Stabilität und Performance bewährt, unter anderem in Hunderten Amazon Services (teils auf JDK 21/17 zurückportiert). Experimente zeigen deutliche Vorteile: bis zu 22 Prozent weniger Heap, 8 Prozent weniger CPU-Zeit, 15 Prozent weniger GCs (G1/Parallel) und ein hoch paralleler JSON-Parser läuft 10 Prozent schneller. Weniger Overhead pro Objekt verbessert die Speichernutzung und Cachelokalität, was spürbar dem Start-up und Durchsatz zugutekommt.
Seit einiger Zeit gibt es neben dem Standard Garbage Collector (G1) auch sogenannte Low Latency GCs wie ZGC und Shenandoah. Sie sind auf moderne Hardwarearchitekturen (Multi-Core-Prozessoren und Terrabyte an RAM) ausgelegt und haben trotz großer Speichermengen kürzere GC-Pausen als Ziel. Mit dem JEP 521 (Generational Shenandoah) wird der generationale Modus des Shenandoah GC (eingeführt als Experiment in JDK 24 via JEP 404) finalisiert. Der Standard bleibt unverändert: Per Default nutzt Shenandoah weiterhin eine Generation, kann aber auf Wunsch auch zwischen Old und Young Generation unterscheiden. Zum Aktivieren des generationalen Modus ist –XX:+UnlockExperimentalVMOptions jetzt nicht mehr nötig. Alle übrigen Optionen und Defaults bleiben gleich, bestehende Startskripte funktionieren weiter. Es gab noch zahlreiche Stabilitäts- und Performanceverbesserungen sowie umfangreiche Tests (u. a. DaCapo, SPECjbb2015, SPECjvm2008, Heapothesys). Anwender berichten von erfolgreichen Einsätzen unter Last.
Erweiterungen beim Kryptografie-Support
Bereits in Java 24 gab es einige JEPs, die Implementierungen von Algorithmen für Schlüsselaustauschverfahren und digitale Signaturen eingeführt haben, um sicher vor zukünftigen Angriffen durch Quantencomputer zu sein. Mit dem JEP 470 (PEM Encodings of Cryptographic Objects) wird nun ein einfaches API eingeführt, um kryptografische Objekte – Schlüssel, Zertifikate und Sperrlisten – in das weit verbreitete PEM-Textformat (RFC 7468) zu kodieren und daraus wieder Objekte zu dekodieren. Es unterstützt die Standardrepräsentationen PKCS#8 (Private Keys), X.509 (Public Keys, Zertifikate, CRLs) sowie PKCS#8 v2.0 (verschlüsselte Private Keys und asymmetrische Schlüssel). Bisher fehlte in Java ein komfortables PEM API, Entwickler mussten Base64, Header/Footer-Parsing, Factory-Auswahl und Algorithmuserkennung selbst erledigen. Das neue Preview-API reduziert diesen Boilerplate deutlich.
Mit dem JEP 510 wird das „Key Derivation Function API“ finalisiert. Es ist unverändert gegenüber der Einführung als Preview in JDK 24. Es handelt sich um ein API für Key Derivation Functions (KDFs), etwa HKDF (RFC 5869) und Argon2 (RFC 9106). Es ermöglicht den Einsatz von KDFs in KEM/HPKE-Szenarien (z. B. ML-KEM, Hybrid Key Exchange in TLS 1.3), erlaubt PKCS#11-basierte Implementierungen und räumt auf, indem JDK-Komponenten wie TLS 1.3 und DHKEM auf das neue API statt auf interne HKDF-Logik umgestellt werden. Aus einem gemeinsamen Geheimnis (z. B. ECDH Shared Secret) und einem Salt kann per HKDF deterministisch ein Satz Sitzungsschlüssel für Verschlüsselung und MAC abgeleitet werden. PBKDF1/2 wandern nicht in das neue API, sie bleiben wie bisher über SecretKeyFactory nutzbar.
Stay tuned
Regelmäßig News zur Konferenz und der Java-Community erhalten
Fazit und Ausblick
Auch Java 25 ist wieder ein spannendes Release mit einigen abgeschlossenen und vielen neuen Features. Auf den ersten Blick ist für uns Entwickler zwar wenig wirklich Neues dabei – vieles sind Wiedervorlagen aus früheren Preview-Versionen. Aber genau das zeigt, wie stabil und durchdacht sich Java weiterentwickelt. Unter der Haube passiert außerdem enorm viel: von Performanceoptimierungen über Sicherheitsverbesserungen bis hin zu Weichenstellungen für die Zukunft, etwa in der Kryptografie und der Speicherverwaltung. Java bleibt damit eine moderne, leistungsfähige Plattform und Sprache. Alle weiteren kleineren Neuerungen, für die es keine JEPs gibt, können in den Release-Notes [3] nachgelesen werden. Änderungen am JDK (Java-Klassenbibliothek) kann man sich zudem sehr schön über den Java Almanac [4] anzeigen lassen.
Ideen für die nächsten Funktionen gehen den JDK-Entwicklern nicht aus. Wer sich vorab über mögliche zukünftige Themen informieren möchte, kann sich schon mal im JEP-Index unter „Draft and submitted JEPs“ [5] umschauen. Besonders interessant sind die „Null-Restricted and Nullable Types“ [6]. Dadurch können wir ähnlich wie bei Kotlin Markierungen an Java-Typen festlegen, ob null-Werte erlaubt sind oder vom Compiler abgewiesen werden sollen. In verschiedenen Vorträgen, insbesondere des Java-Language-Architekten Brian Goetz, verdichten sich zudem die Anzeichen, dass sie beim Projekt Valhalla kurz vor dem finalen Durchbruch stehen. Dann könnten die Value Types also bald Wirklichkeit werden und eine neue Ära in der Programmierung mit Java einläuten.
Im März 2026 wird mit dem OpenJDK 26 schon die nächste Version erscheinen. Zum Zeitpunkt der Erstellung dieses Artikels ist bisher nur ein JEP eingeplant und zwar die endgültige Entfernung des Applet API (JEP 504 [7]). Hier sieht man die Bereitschaft, alte Zöpfe abzuschneiden. Und auch wenn man Applets in modernen Browsern schon länger nicht mehr nutzen konnte, wird der ein oder andere Java-Veteran diese Entwicklung auch mit einem weinenden Auge beobachten.
Und noch ein Fun Fact zum Schluss: Wenn Oracle im nächsten Jahr auf einen jährlichen Zyklus wechselte, dann ließen sich die nächsten Versionsnummern immer an der Jahreszahl ihrer Erscheinung ablesen (27 in 2027 …). Aber diesen Gefallen werden sie uns vermutlich nicht tun, dafür funktioniert das halbjährliche Releasemodell einfach zu gut. Eine Alternative wäre dann höchstens noch, dass man 26.a und 26.b herausbringt. Aber das würde das bisherige Versionierungsschema kaputt machen. Genießen wir also die zwei Jahre, in denen die Versionsnummer der Jahreszahl entspricht. Und ab Herbst 2026 müssen wir dann wieder herumrechnen, welches denn eigentlich die aktuelle Java-Version ist.
Frequently Asked Questions (FAQ)
1. Was zeichnet Java 25 als Release aus?
Java 25 ist ein Long-Term-Support (LTS)-Release mit 18 realisierten JDK Enhancement Proposals (JEPs).
2. Welche JEPs von Java 25 wurden finalisiert, die zuvor als Preview existierten?
Finalisiert wurden unter anderem Scoped Values (JEP 506), Module Import Declarations (JEP 511), Compact Source Files & Instance‑Main (JEP 512) und Flexible Constructor Bodies (JEP 513).
3. Wofür dienen „Stable Values“ in Java 25?
Stable Values (JEP 502) ermöglichen eine “deferred immutability” – also eine verzögerte, threadsichere Initialisierung, die mehr Flexibilität als final‑Felder bietet und gleichzeitig Start‑ und Aufwärmzeiten verkürzt.
4. Welche Verbesserungen bringt Java 25 für die Nebenläufigkeit?
Java 25 finalisiert Scoped Values (JEP 506) und bietet strukturierte Nebenläufigkeit über Structured Concurrency in der fünften Preview (JEP 505), was fehleranfällige Executor‑Setups ersetzt.
5. Wie optimiert Java 25 Speicherverbrauch und Performance durch JEPs?
Compact Object Headers (JEP 519) reduzieren Objekt‑Header von 96 auf 64 Bit, was sowohl Heap‑Nutzung als auch CPU‑Last und Garbage‑Collection‑Zyklen deutlich verringert.
6. Welche Profiling‑ und Observability‑Verbesserungen enthält Java 25?
Die JDK Flight Recorder‑Erweiterungen umfassen CPU-Time Profiling (JEP 509), Cooperative Sampling (JEP 518) und Method Timing & Tracing (JEP 520) für präzisere und robustere Profilerstellung.
7. Welche kryptografischen Features sind neu in Java 25?
Java 25 führt Preview‑Support für PEM Encodings of Cryptographic Objects (JEP 470) sowie die finale Key Derivation Function API (JEP 510) für moderne Kryptoszenarien ein.
8. Was sind weitere relevante Änderungen in Java 25?
Weitere wichtige Neuerungen umfassen die Entfernung des 32‑Bit‑x86‑Ports (JEP 503), AOT‑Verbesserungen (JEP 514 & JEP 515) und die finale Generational‑Shenandoah‑GC‑Unterstützung (JEP 521) sowie weitere APIs wie Vector API (JEP 508) und primitive patterns (JEP 507).
Links & Literatur
[1] https://openjdk.java.net/projects/jdk/25/
[2] https://openjdk.org/jeps/508
[3] https://jdk.java.net/25/release-notes
[4] https://javaalmanac.io/jdk/25/apidiff/24/
[5] https://openjdk.org/jeps/0#Draft-and-submitted-JEPs
[6] https://bugs.openjdk.org/browse/JDK-8303099
[7] https://openjdk.org/jeps/504?v=Dhn-JgZaBWoa/2024/03/19/announcing-javaone-2025/