
Что нового в Java 25. Часть 1
Выход Java 25 LTS назначен на 16 сентября. Это первый LTS-релиз с выхода Java 21 в сентябре 2023 года. Java 25 LTS получит минимум пять лет поддержки от Axiom.
Релиз-кандидат уже доступен, а список фичей заморожен. В JDK 25 нас ожидает 18 JEP, охватывающих язык, библиотеки, многопоточность, профилирование, производительность и сборку мусора.
Скачать Axiom JDK 25 вы сможете в личном кабинете после его официального релиза.
Из-за количества JEP в релизе мы решили разделить статью на несколько частей. В первой части мы рассмотрим 6 финализированных JEP, которые вошли в новую Java.
Содержание
- Категории JEP
- Как использовать фичи в предварительной и экспериментальной версиях
- JEP 521: Generational Shenandoah GC
- JEP 519: Компактные заголовки объектов (Compact Object Headers)
- JEP 506: Scoped Values
- JEP 511: Объявление импорта модулей (Module Import Declarations)
- JEP 503: Удаление 32-битного порта x86-систем
- Заключение
Категории JEP
- Язык: Compact Source Files & Instance Main Methods (JEP 512), Flexible Constructor Bodies (JEP 513), Module Import Declarations (JEP 511), Primitive Types in Patterns (JEP 507).
- Библиотеки: Scoped Values (JEP 506), Key Derivation Function API (JEP 510), PEM Encodings (JEP 470).
- Многопоточность: Structured Concurrency (JEP 505).
- Производительность и рантайм: Compact Object Headers (JEP 519), Generational Shenandoah (JEP 521), AOT-связанные улучшения (JEP 514, 515).
- Профилирование: JFR Cooperative Sampling (JEP 518), JFR CPU-Time Profiling (JEP 509), JFR Method Timing (JEP 520).
- Прочее: Stable Values (JEP 502), Vector API (JEP 508), удаление 32-битного порта x86 (JEP 503).
Как использовать фичи в предварительной и экспериментальной версиях
Чтобы попробовать экспериментальные фичи или фичи в предварительной версии, нужно их явно включить. Вы можете это сделать как через командную строку, так и настроить в интегрированной среде разработки (IDE). Некоторые фичи можно включать на уровне javac
, а не только при запуске java
.
В командной строке включите фичу в предварительной версии одним из следующих способов:
- Скомпилируйте программу с помощью
javac --release 24 --enable-preview Main.java
и запустите ее с помощьюjava --enable-preview Main
. - При использовании source code launcher запустите программу с
java --enable-preview Main.java
. - При использовании jshell запустите его с помощью
jshell --enable-preview
.
JEP 521: Generational Shenandoah GC
Режим поколений для сборщика мусора Shenandoah GC был представлен в JDK 24 (JEP 404) в качестве экспериментальной фичи, а утвердится в JDK 25. Режим работы Shenandoah GC по умолчанию (режим одного поколения, single generation) не изменится.
Shenandoah GC — это сборщик мусора с низкой задержкой: он почти всю работу делает параллельно с вашим приложением, включая сжатие данных в памяти. Поэтому паузы при его работе остаются короткими, даже если куча (heap) огромная.
Изначально Shenandoah был non-generational, то есть не делил объекты на молодые и старые, как G1 GC, Parallel GC и ZGC (с JDK 23). Из-за этого приходилось держать больше свободной памяти и тратить больше времени на обработку «долго живущих» объектов.
Теперь появился generational Shenandoah, который сможет поддерживать молодое и старое поколения, позволяя чаще собирать молодые объекты. Shenandoah будет собирать либо только молодые объекты, либо одновременно и молодые, и старые, как G1 GC. Shenandoah GC выполняет смешанные сборки (mixed collections) с учётом уникальной модели работы с регионами и барьерами. Это снижает нагрузку и делает работу ещё стабильнее. При этом главный плюс Shenandoah остаётся — паузы не зависят от размера кучи, потому что сборка идёт вместе с работой приложения.
Кроме того, новый режим сам подстраивает размеры поколений и связанные параметры работы, чтобы экономить память и держать задержки как можно меньше.
Чтобы включить режим поколений для Shenandoah GC, укажите следующие параметры JVM:
$ java -XX:+UseShenandoahGC \
-XX:ShenandoahGCMode=generational ...
JEP 519: Компактные заголовки объектов (Compact Object Headers)
В JDK 24 (JEP 450) были представлены компактные заголовки объектов, которые утвердятся в JDK 25.
Цель компактных заголовков объекта — уменьшить размер заголовков Java-объектов в HotSpot JVM с 96 или 128 бит до 64 бит на 64-битных архитектурах (x64 и AArch64). Это призвано уменьшить использование CPU и/или памяти Java-приложениями.
Ряд экспериментов показал, что включение компактных заголовков объектов улучшает производительность:
- В одном сценарии бенчмарк SPECjbb2015 использовал на 22% меньше памяти кучи и на 8% меньше процессорного времени.
- В другом сценарии количество сборок мусора, выполненных SPECjbb2015, снизилось на 15% как для сборщика G1, так и для Parallel GC.
- Бенчмарк высокопараллельного JSON-парсера выполнялся на 10% быстрее.
Напоминаем, что результат зависит от ваших объектов. Если у вас много маленьких объектов, то эффект будет ощутимым, а если объекты большие (данные больше заголовка), то ощутимого эффекта вы не получите.
Чтобы включить компактные заголовки объектов, укажите опцию -XX:+UseCompactObjectHeaders
в командной строке:
$ java -XX:+UseCompactObjectHeaders ...
JEP 506: Scoped Values
Scoped Values появились в инкубаторе в Java 20, остались на второе, третье и четвёртое превью, а теперь утвердятся в Java 25 с одним небольшим изменением: метод ScopedValue.orElse
больше не принимает null
в качестве аргумента.
При разработке Java-приложений нередко возникает задача передать данные между методами. Например, веб-фреймворк принимает входящие HTTP-запросы и вызывает обработчик приложения, чтобы их обработать. Обработчик приложения снова обращается к фреймворку, чтобы получить данные из базы данных или вызвать другой HTTP-сервис.
Фреймворк может использовать объект FrameworkContext
, который содержит ID аутентифицированного пользователя, ID транзакции и др., и связывать его с текущей транзакцией. Все операции фреймворка работают с объектом FrameworkContext
, но для пользовательского кода этот объект не имеет значения.
Обычно в таком случае используют переменные типа ThreadLocal
:
public class Framework {
private final Application application;
public Framework(Application app) { this.application = app; }
private static final ThreadLocal<FrameworkContext> CONTEXT
= new ThreadLocal<>(); // объявляем переменную типа ThreadLocal
void serve(Request request, Response response) {
var context = createContext(request);
CONTEXT.set(context); // записываем значение FrameworkContext в переменную
Application.handle(request, response);
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get(); // считываем значение переменной, чтобы получить FrameworkContext
var db = getDBConnection(context);
db.readKey(key);
}
}
Переменная типа Threadlocal
служит скрытым параметром метода. Поток, в котором вызывается CONTEXT.set
в Framework.serve
, а затем CONTEXT.get
в Framework.readKey
, автоматически увидит локальную копию переменной CONTEXT
. По сути, поле ThreadLocal
— ключ для поиска значения FrameworkContext
для текущего потока.
Однако у ThreadLocal
есть несколько недостатков:
- Неконтролируемая изменяемость. Любую переменную типа
ThreadLocal
можно изменить в любое время через методset()
, даже если она хранит неизменяемый объект. - Неограниченный срок жизни. Значение переменной
ThreadLocal
сохраняется до завершения исполнения потока или до явного вызова методаremove()
, про который часто забывают. Это может привести к утечкам памяти, особенно при пуле потоков. Если переменные активно изменяются, будет сложно определить, когда безопасно вызватьremove()
. - Дорогое наследование. Когда дочерние потоки наследуют переменные
ThreadLocal
родительского потока, для каждого дочернего потока требуется отдельное хранилище для всех переменныхThreadLocal
родителя. Это увеличивает использование памяти и ресурсов, даже если эти переменные редко изменяются в дочерних потоках.
Использование виртуальных потоков усложнит ситуацию, поскольку число виртуальных потоков может быть сильно больше, чем обычных потоков.
Проблемы использования ThreadLocal
решаются ScopedValue
. Значение в ScopedValue
в отличие от ThreadLocal
записывается один раз и доступно только в течение ограниченного периода времени во время исполнения потока.
1) Создаётся объект ScopedValue
, не связанный ни с одним потоком.
2) В коде вызывается метод where()
, который принимает значение (scoped value) и объект ScopedValue
.
3) Затем метод run()
привязывает значение к объекту, создавая копию scoped value для текущего потока, и запускает переданное лямбда-выражение. Пока выполняется метод run()
, лямбда-выражение и любые методы, вызванные из него напрямую или косвенно, могут получать доступ к scoped value через метод get()
. После завершения метода run()
scoped value отвязывается от объекта.
С помощью ScopedValue
пример с фреймворком можно переписать следующим образом:
class Framework {
private static final ScopedValue<FrameworkContext> CONTEXT
= ScopedValue.newInstance(); // объявляем переменную типа ScopedValue
void serve(Request request, Response response) {
var context = createContext(request);
ScopedValue.where(CONTEXT, context) // вызываем метод where...run вместо метода ThreadLocal.set()
.run(() -> Application.handle(request, response));
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get(); // считываем значение переменной
var db = getDBConnection(context);
db.readKey(key);
}
}
В общем случае, рекомендуется переходить c ThreadLocal
на ScopedValue
, когда цель использования — однонаправленная передача неизменяемых данных. Однако есть несколько сценариев, в которых лучше использовать ThreadLocal
. Например, кэширование объектов, которые дорого создавать и использовать.
Scoped value API доступно по ссылке.
JEP 511: Объявление импорта модулей (Module Import Declarations)
Объявление импорта модулей было представлено в JDK 23 (JEP 476) в предварительной версии, затем улучшено в JDK 24 (JEP 494) и утвердится в JDK 25.
Эта фича позволяет вместо явного импортирования отдельных пакетов, классов и интерфейсов импортировать модуль со всеми его публичными классами и интерфейсами, а также модулями, от которых он зависит транзитивно. Это упростит переиспользование модульных библиотек путём одновременного импортирования модулей, снизит необходимость в множестве отдельных import-директив и упростит работу с кодом, особенно на ранних этапах разработки.
Импорт модуля выглядит следующим образом:
import module M;
Так импортируются все публичные классы и интерфейсы верхнего уровня из пакетов, экспортируемых модулем M
, и его транзитивных зависимостей в текущий модуль.
Например, для работы с коллекциями и потоками требовалось вручную импортировать каждую зависимость:
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
String[] fruits = { "apple", "berry", "citrus" };
Map<String, String> m = Stream.of(fruits)
.collect(Collectors.toMap(s -> s.toUpperCase().substring(0, 1), Function.identity()));
С выходом Java 9 появилась система модулей, которая объединяет набор пакетов в единое целое. Логично было бы иметь возможность импортировать сразу весь модуль.
Например, import module java.base;
даст доступ к List
, Map
, Stream
и Path
без десятка отдельных import java.util.*
, import java.util.stream.*
и т. д.
Особенно это полезно в ситуациях, когда API одного модуля тесно связано с API другого. Например, java.sql
зависит от java.xml
. С новым синтаксисом достаточно написать import module java.sql;
, чтобы импортировать всё нужное.
Теперь можно просто импортировать весь модуль java.base
, содержащий List
, Map
, Stream
, Path
и другие классы:
import module java.base;
String[] fruits = { "apple", "berry", "citrus" };
Map<String, String> m = Stream.of(fruits)
.collect(Collectors.toMap(s -> s.toUpperCase().substring(0, 1), Function.identity()));
Если один модуль экспортирует классы с одинаковыми именами, компилятор выдаст ошибку. Например:
import module java.base; // java.util.List
import module java.desktop; // java.awt.List
List l = ...; // ошибка: неоднозначность
Решение — использовать явный импорт класса нужного модуля:
import java.util.List; // теперь всё однозначно
Ещё вариант — использовать более широкий import package.*
, который затмит модульный импорт.
Приоритеты следующие:
- Однотипный импорт (наиболее специфичный).
- Пакетный импорт *.
- Импорт модуля (наименее специфичный).
Также раньше приходилось писать:
import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.stream.*;
Теперь достаточно:
import module java.xml;
При использовании компактных исходных файлов (JEP 512) модуль java.base
импортируется автоматически. Это значит, что List
, Map
, Stream
и прочие сразу доступны без необходимости отдельного импорта их соответствующих пакетов.
В JShell список автоподключаемых пакетов будет заменён на import module java.base;
.
java.se
не экспортирует пакеты сам по себе, но транзитивно тянет 19 модулей.
import module java.se;
Теперь это приведёт к импорту всех стандартных API Java, включая java.base
.
Разработчики этого JEP считают, что импорт на уровне модулей делает код чище и удобнее, особенно при использовании больших API. Однако возможны конфликты имён, которые решаются с помощью явного импорта.
Вообще говоря, традиции не рекомендуют использовать даже конструкции вида import bla.*
.
В таком ключе import module
начинает играть новыми красками. Хорошо, если используется продвинутая IDE, которая всё сама подскажет и подсветит. Если же вы смотрите код в блокноте, терминале или в веб-интерфейсе для код-ревью, то начинается игра в угадайку.
Также импорт модулей не из поставки JDK — это лотерея, в которой мы не знаем, что же этот модуль транзитивно с собой принесёт.
JEP 503: Удаление 32-битного порта x86-систем
В JDK 25 будет удалён исходный код и прекращена поддержка сборки для 32-битного порта систем x86. В JDK 24 (JEP 501) этот порт был помечен как подлежащий удалению (deprecated).
Причина удаления — стоимость поддержки этого порта превышает его пользу.
Предполагается, что индустрия окончательно перешла в 64-битный мир. Новое 32-битное x86-железо больше не производится, а оставшиеся развёртывания 32-битного x86 — это наследие. Windows 10, последняя версия Windows с поддержкой 32-битных систем, перестанет поддерживаться в октябре 2025 года, а порт Windows 32-битного x86 уже удалён из JDK (JEP 479). Debian также планирует прекратить поддержку 32-битного x86 в ближайшем будущем.
32-битный порт x86 всё ещё можно собрать, хотя он не поддерживается и не гарантирует адекватной производительности.
Удаление порта позволит разработчикам ускорить внедрение новых функций и улучшений.
JEP 510: Key Derivation Function API
В JDK 24 (JEP 478) в пакете javax.crypto
появилось новое API, реализующее функции генерации производного ключа (Key Derivation Function, KDF) — javax.crypto.KDF
. В JDK 25 это API утвердится без изменений.
Функции выработки ключей (KDF) используют криптографические входные данные (например, исходный материал для ключа, значение соли и псевдослучайную функцию) для создания нового криптографически стойкого ключевого материала. С KDF можно получать несколько ключей из одного источника, что обеспечит безопасность и возможность воспроизведения при наличии одинаковых входных данных у двух сторон.
API KEM (JEP 452), интегрированный в JDK 21, вместе с KDF API являются важными шагами для поддержки гибридного шифрования с открытым ключом (Hybrid Public Key Encryption, HPKE) в Java. Этот алгоритм шифрования обеспечивает плавный переход к алгоритмам, устойчивым к квантовым атакам.
Стандарт PKCS#11 для криптографических аппаратных устройств уже много лет описывает поддержку KDF. Доступность KDF через javax.crypto
даст удобство для приложений и библиотек, работающих с такими устройствами. Разработчики сторонних криптопровайдеров также смогут предлагать свои реализации KDF.
С KDF API Java-платформа даст улучшенную поддержку для современных функций выработки ключей на основе паролей, более совершенных, чем PBKDF1 и PBKDF2, например Argon2. Ни один из существующих криптографических API в Java не способен “из коробки” представлять KDF.
Класс KDF определяет два метода для выработки ключей:
deriveKey(String alg, AlgorithmParameterSpec spec)
— создаёт объектSecretKey
, используя указанный алгоритм и параметры.deriveData(AlgorithmParameterSpec spec)
— возвращает массив байтов, который может быть использован как энтропия или как ключевой материал в такой форме.
Используется пустой интерфейс AlgorithmParameterSpec
, так как различные алгоритмы KDF требуют разных параметров. Реализация KDF должна определить один или несколько подклассов AlgorithmParameterSpec
для описания своих параметров.
Для включённой реализации HKDF предоставляются три таких подкласса, каждый из которых соответствует определённому режиму работы:
Интерфейс HKDFParameterSpec
содержит статические фабричные методы для создания экземпляров этих трёх классов, а также класс KDFParameterSpec.Builder
для сборки ключевого материала для операций извлечения.
// Создание объекта KDF для указанного алгоритма
KDF hkdf = KDF.getInstance("HKDF-SHA256");
// Создание спецификации параметров Extract-Expand
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial)
.addSalt(salt).thenExpand(info, 32);
// Выработка 32-байтового ключа AES
SecretKey key = hkdf.deriveKey("AES", params);
// Можно вызывать deriveKey повторно с тем же объектом KDF
В будущем планируется добавить поддержку Argon2.
KDF API лучше подходит для выведения ключей, чем старое API на основе классов KeyGenerator и SecretKeyFactory. KeyGenerator рассчитан на добавление энтропии через SecureRandom для генерации недетерминированного ключа из входных данных, а KDF предполагает возможность независимой детерминированной выработки ключей двумя сторонами. Также SecretKeyFactory рассчитан на создание одного ключа, а KDF должен поддерживать последовательные детерминированные выработки из потока ключевого материала.
Заключение
Мы рассмотрели первую часть JEP в Java 25. Во второй части мы рассмотрим больше JEP и посмотрим куда развивается Java.