
Что нового в Java 24. Часть 3
Это третья и заключительная часть обзора JEP в Java 24. Если вы пропустили первые две части, см. Что нового в Java. Часть 1 и Что нового в Java. Часть 2.
В этой части мы рассмотрим оставшиеся восемь фичей, среди которых долгожданный Project Leyden, Structured Concurrency, Key Derivation Function API и другие.
Содержание
- Что нового в Java 24. Часть 3
- Как начать использовать фичи в предварительной и экспериментальной версиях
- Новые фичи
- Улучшенные фичи
- Подготовка к ограничению и удалению
- Заключение
Как начать использовать фичи в предварительной и экспериментальной версиях
Чтобы попробовать экспериментальные фичи или фичи в предварительной версии, нужно их явно включить. Вы можете это сделать как через командную строку, так и настроить в интегрированной среде разработки (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 478: Key Derivation Function API (предварительная версия)
В пакете javax.crypto
появилось новое API, реализующее функции генерации производного ключа (Key Derivation Function, KDF) — javax.crypto.KDF
.
С развитием квантовых вычислений классические криптографические алгоритмы становятся уязвимыми. Java должна поддерживать постквантовую криптографию (Post-Quantum Cryptography, PQC), устойчивую к таким атакам. Планируется реализация Hybrid Public Key Encryption (HPKE), обеспечивающей плавный переход на квантово-устойчивые алгоритмы. Механизм Key Encapsulation Mechanism (KEM) API (JEP 452), представленный в JDK 21, стал первым шагом в этом направлении. Новый KDF API в рамках JEP 478 является следующим этапом.
Функция KDF формирует один или несколько криптографически стойких секретных ключей на основе заданного секретного значения с помощью псевдослучайной функции. KDF позволяет задавать длину ключа, создаваемого в результате работы, а стойкость такого ключа будет такой же, как и у случайного ключа той же длины. С KDF можно получать несколько ключей из одного источника, обеспечивая безопасность и возможность воспроизведения при наличии одинаковых входных данных у двух сторон.
KDF выполняет две основные операции:
- Создание и инициализация — создание объекта KDF и его настройка с нужными параметрами.
Класс KDF предоставляет стандартные методы getInstance()
, которые принимают имя алгоритма, а также, при необходимости, параметры KDF (KDFParameters) и криптопровайдера (Provider). В рамках этого JEP реализован только алгоритм HMAC-based Extract-and-Expand Key Derivation Function (HKDF).
Пример:
KDF hkdf = KDF.getInstance("HKDF-SHA256");
- Генерация ключей — использование заданного секретного значения и дополнительных параметров, включая параметры описания выходного значения, для получения производного ключа или данных.
Процесс генерации ключей схож с хешированием паролей: KDF использует хеш-функцию с дополнительной энтропией для извлечения нового материала для будущего ключа или его расширения в поток данных.
Класс KDF предоставляет два метода для генерации ключей:
-
deriveKey(String alg, AlgorithmParameterSpec spec) — создаёт объект
SecretKey
посредством указанного алгоритма и в соответствии с указанными параметрами.. -
deriveData(AlgorithmParameterSpec spec) — возвращает массив байтов, который может использоваться для энтропии или в качестве материала для ключа, если ключ нужен в такой форме.
API использует AlgorithmParameterSpec
в качестве универсального контейнера для параметров KDF, а конкретные алгоритмы предоставляют свои реализации.
Пример использования HKDF:
// Создаётся объект KDF для указываемого алгоритма
KDF hkdf = KDF.getInstance("HKDF-SHA256");
// Создаётся параметр спецификации ExtractExpand
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial)
.addSalt(salt).thenExpand(info, 32);
// Создаётся 32-битный AES-ключ
SecretKey key = hkdf.deriveKey("AES", params);
У одного и того же объекта KDF можно вызывать метод deriveKey()
несколько раз.
Реализация KDF призвана расширить абстрактный класс javax.crypto.KDFSpi
.
Некоторые алгоритмы KDF генерируют несколько ключей за один вызов. В этом случае рекомендуется либо возвращать массив байтов с разделением ключей и задокументировать, как их разделять, либо создать подкласс SecretKey
с методами доступа к каждому ключу.
KDF API находится в режиме предварительного просмотра и отключён по умолчанию, поэтому для его использования необходимо включить поддержку фичей в предварительной версии.
JEP 483: Ahead-of-Time Class Loading & Linking
Java-платформа динамична, что делает её мощной, но это приводит к задержкам при старте приложения из-за:
- Загрузки и парсинга сотен JAR-файлов.
- Линковки классов, разрешения символических ссылок и валидации байткода.
- Выполнения статических инициализаторов классов.
- Использования рефлексии.
Решение: Перенос части работы из времени выполнения (just-in-time) на этап компиляции (ahead-of-time, AOT).
Расширение HotSpot JVM для поддержки AOT-кэша:
-
Первая стадия (“обучение”): запуск приложения с записью AOT-конфигурации.
-
Вторая стадия: создание кэша на основе AOT-конфигурации.
-
Третья стадия: использование AOT-кэша при последующих запусках.
Пример создания AOT-кэша:
# Запись AOT-конфигурации
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -cp app.jar com.example.App
# Создание кэша
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot -cp app.jar
# Запуск с использованием кэша
java -XX:AOTCache=app.aot -cp app.jar com.example.App
Рассмотрим пример. Программа использует Stream API, загружая около 600 классов:
import java.util.*;
import java.util.stream.*;
public class HelloStream {
public static void main(String ... args) {
var words = List.of("hello", "fuzzy", "world");
var greeting = words.stream()
.filter(w -> !w.contains("z"))
.collect(Collectors.joining(", "));
System.out.println(greeting); // hello, world
}
}
Результаты тестирования:
- Без AOT-кэша: 0.031 сек.
- С AOT-кэшем: 0.018 сек. (ускорение на 42%).
Для серверного приложения Spring PetClinic (21 000 классов):
- Без AOT-кэша: 4.486 сек.
- С AOT-кэшем: 2.604 сек. (ускорение на 42%).
Как эффективно обучить JVM:
- Использовать тренировочный запуск, имитирующий продакшен.
- Исключить ненужные классы для уменьшения размера кэша.
- Проверить загруженные классы с помощью
-verbose:class
.
Совместимость:
- AOT-кэш работает со всеми Java-приложениями без изменения исходного кода.
- Требует одинакового JDK, архитектуры и ОС на всех этапах.
- Поддерживает рефлексию и динамическую природу Java.
Использование AOT-кэша значительно сокращает время запуска Java-приложений, особенно серверных, за счёт переноса загрузки и линковки классов на предварительный этап. При обновлении приложения требуется перестройка кеша.
JEP 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
Квантовые вычисления активно развиваются, и в будущем масштабный квантовый компьютер сможет использовать алгоритм Шора для факторизации целых чисел и решения задачи дискретного логарифма. Это угрожает безопасности широко применяемых криптографических алгоритмов, таких как RSA и Диффи-Хеллман, которые используются в Java для цифровой подписи JAR-файлов и установления защищённых соединений через TLS.
Даже если такие компьютеры ещё не созданы, атакующий может перехватывать зашифрованные данные сегодня, чтобы расшифровать их в будущем. В ответ на эту угрозу разработаны квантово-устойчивые алгоритмы, невосприимчивые к алгоритму Шора. Для безопасного обмена ключами NIST стандартизировал Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM) в FIPS 203.
ML-KEM включает три основных функции:
- Генерация ключевой пары: создаёт открытый и закрытый ключи.
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-KEM");
g.initialize(NamedParameterSpec.ML_KEM_512);
KeyPair kp = g.generateKeyPair(); // ML-KEM-512
Если параметр (как NamedParameterSpec.ML_KEM_512
выше) не задан, используется ML-KEM-768 по умолчанию:
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-KEM");
KeyPair kp = g.generateKeyPair(); // ML-KEM-768
Можно явно указать алгоритм шифрования:
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-KEM-1024");
KeyPair kp = g.generateKeyPair(); // ML-KEM-1024
Важно: передача числового размера ключа приведёт к ошибке InvalidParameterException
.
Генерация ключевой пары через keytool
:
$ keytool -keystore ks -storepass changeit -genkeypair -alias mlkem \
-keyalg ML-KEM -groupname ML-KEM-768 -dname CN=ML-KEM -signer ec
Примечание: ML-KEM не является алгоритмом подписи, поэтому для подписания сертификатов используются, например, EC-ключи.
- Инкапсуляция ключа (используется отправителем): принимает открытый ключ получателя и возвращает секретный ключ K и сообщение инкапсуляции.
Отправитель (инкапсуляция ключа):
KEM ks = KEM.getInstance("ML-KEM");
KEM.Encapsulator enc = ks.newEncapsulator(publicKey);
KEM.Encapsulated encap = enc.encapsulate();
byte[] msg = encap.encapsulation(); // Отправить получателю
SecretKey sks = encap.key(); // Секретный ключ
- Декапсуляция ключа (используется получателем): принимает закрытый ключ и сообщение инкапсуляции и извлекает секретный ключ K.
Получатель (декапсуляция ключа):
KEM kr = KEM.getInstance("ML-KEM");
KEM.Decapsulator dec = kr.newDecapsulator(privateKey);
SecretKey skr = dec.decapsulate(msg); // Извлекаем секретный ключ
sks
и skr
содержат один и тот же ключ, известный только отправителю и получателю.
Если KEM создаётся с параметром "ML-KEM"
, он принимает любые ключи ML-KEM. Если указывается конкретный параметр (например, "ML-KEM-1024"
), поддерживаются только ключи с этим параметром, иначе выбрасывается InvalidKeyException
.
В Java реализуются:
- KeyPairGenerator для генерации пар ключей ML-KEM.
- KEM API для обмена секретными ключами с использованием ML-KEM.
- KeyFactory API для кодирования и декодирования ключей ML-KEM.
Определяется стандартное имя алгоритма “ML-KEM” для KeyPairGenerator, KEM и KeyFactory API.
FIPS 203 указывает три уровня параметров для ML-KEM, балансирующих между безопасностью и производительностью:
- ML-KEM-512 (минимальная безопасность, лучшая производительность),
- ML-KEM-768 (средний уровень, по умолчанию в Java),
- ML-KEM-1024 (наибольшая безопасность, но медленнее).
Разберём кодирование и декодирование ключей ML-KEM. Конвертация приватного ключа ML-KEM в PKCS #8 и обратно:
KeyFactory f = KeyFactory.getInstance("ML-KEM");
PKCS8EncodedKeySpec p8spec = f.getKeySpec(kp.getPrivate(), PKCS8EncodedKeySpec.class);
PrivateKey sk2 = f.generatePrivate(p8spec);
Конвертация публичного ключа ML-KEM в X.509 и обратно:
X509EncodedKeySpec x509spec = f.getKeySpec(kp.getPublic(), X509EncodedKeySpec.class);
PublicKey pk2 = f.generatePublic(x509spec);
Если KeyFactory создаётся с "ML-KEM"
, он работает с любыми ключами ML-KEM (ML-KEM-512, ML-KEM-768 или ML-KEM-1024). Если указывается конкретный параметр ML-KEM (например, "ML-KEM-1024"
), то поддерживаются только ключи с этим параметром. В противном случае методы translateKey
, generatePrivate
, generatePublic
, getKeySpec
выбрасывают исключения InvalidKeyException
или InvalidKeySpecException
.
Формат кодирования ML-KEM KeyFactory описан в проекте IETF RFC и может измениться до его официальной публикации.
Добавление поддержки ML-KEM в Java призвано обеспечить защиту от будущих квантовых угроз, сохраняя совместимость и простоту интеграции в экосистему JDK.
JEP 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
В рамках JEP 497 был внедрён алгоритм для безопасного обмена ключами и подписания данных Module-Lattice-Based Digital Signature Algorithm (ML-DSA).
ML-DSA включает три основных компонента:
- KeyPairGenerator API — генерация пар ключей ML-DSA.
- Signature API — подпись данных и верификация подписей.
- KeyFactory API — кодирование и декодирование ключей ML-DSA.
Определяется стандартное имя алгоритма “ML-DSA” для KeyPairGenerator, Signature и KeyFactory API. FIPS 204 указывает три уровня параметров для ML-DSA в порядке повышения уровня безопасностью и уменьшения производительности:
- ML-DSA-44 (минимальная безопасность, лучшая производительность).
- ML-DSA-65 (средний уровень, по умолчанию в Java).
- ML-DSA-87 (наибольшая безопасность, но медленнее).
Чтобы сгенерировать пару ключей ML-DSA, используйте один из трёх способов:
- Создать экземпляр
KeyPairGenerator
с параметром"ML-DSA"
и инициализировать его требуемым параметром ML-DSA:
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-DSA");
g.initialize(NamedParameterSpec.ML_DSA_44);
KeyPair kp = g.generateKeyPair(); // ML-DSA-44
- Создать экземпляр
KeyPairGenerator
с параметром"ML-DSA"
и не инициализироватьKeyPairGenerator
. Тогда по умолчанию будет выбран параметр ML-DSA-65:
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-DSA");
KeyPair kp = g.generateKeyPair(); // ML-DSA-65
- Создать экземпляр
KeyPairGenerator
с явным параметром ML-DSA:
KeyPairGenerator g = KeyPairGenerator.getInstance("ML-DSA-87");
KeyPair kp = g.generateKeyPair(); // ML-DSA-87
Важно: передача числового размера ключа в KeyPairGenerator
приведёт к InvalidParameterException
.
Сгенерировать пары ключей ML-DSA можно также через keytool
:
$ keytool -keystore ks -storepass changeit -genkeypair -alias mldsa \
-keyalg ML-DSA -groupname ML-DSA-65 -dname CN=ML-DSA
Можно напрямую передать требуемый параметр ML-DSA-65 через -keyalg
:
$ keytool -keystore ks -storepass changeit -genkeypair -alias mldsa \
-keyalg ML-DSA-65 -dname CN=ML-DSA2
Чтобы подписать и верифицировать подписи ML-DSA, используйте Signature
.
Подписание данных:
byte[] msg = ...;
Signature ss = Signature.getInstance("ML-DSA");
ss.initSign(privateKey);
ss.update(msg);
byte[] sig = ss.sign();
Проверка подписи:
byte[] msg = ...;
byte[] sig = ...;
Signature sv = Signature.getInstance("ML-DSA");
sv.initVerify(publicKey);
sv.update(msg);
boolean verified = sv.verify(sig);
Если Signature
создаётся с алгоритмом "ML-DSA"
, он принимает любые ключи ML-DSA. Если указывается конкретный параметр (например, "ML-DSA-87"
), поддерживаются только ключи с этим параметром, иначе методы initSign
и initVerify
выбрасывается InvalidKeyException
.
Разберём кодирование и декодирование ключей ML-DSA. Вы можете конвертировать приватный ключ ML-DSA в PKCS #8 и обратно:
KeyFactory f = KeyFactory.getInstance("ML-DSA");
PKCS8EncodedKeySpec p8spec = f.getKeySpec(kp.getPrivate(), PKCS8EncodedKeySpec.class);
PrivateKey sk2 = f.generatePrivate(p8spec);
Похожим образом вы можете конвертировать публичный ключ ML-DSA в X.509 и обратно:
X509EncodedKeySpec x509spec = f.getKeySpec(kp.getPublic(), X509EncodedKeySpec.class);
PublicKey pk2 = f.generatePublic(x509spec);
Если KeyFactory
создаётся с параметром "ML-DSA"
, он работает с ключами любого параметра. Если указывается конкретный набор параметров, поддерживаются только ключи с этим параметром. В противном случае методы translateKey
, generatePrivate
, generatePublic
, getKeySpec
выбрасывают исключения InvalidKeyException
или InvalidKeySpecException
.
Формат кодирования ML-DSA KeyFactory
описан в проекте IETF RFC и может измениться до его официальной публикации.
Добавление поддержки ML-DSA в Java призвано обеспечить защиту от будущих квантовых угроз, сохраняя совместимость и простоту интеграции в экосистему JDK.
Улучшенные фичи
JEP 491: Synchronize Virtual Threads without Pinning
JEP 491 представляет важное улучшение для виртуальных потоков.
Виртуальные потоки могут монтироваться на платформенные потоки и демонтироваться без их блокировки, что позволяет нескольким виртуальным потокам совместно использовать один поток ОС.
Однако есть случаи, в которых виртуальные потоки “прикрепляются” к платформенным потокам, из-за чего потоки не могут демонтироваться. Так происходит в следующих ситуациях:
- Виртуальный поток запускает код в блоке или методе synchronized.
- Виртуальный поток запускает нативный метод или функцию с использованием Foreign Function and Memory API.
Мы сосредоточимся на synchronized-методах. Рассмотрим следующий пример:
synchronized byte[] getData() {
byte[] buf = ...;
int nread = socket.getInputStream().read(buf); // Блокирующая операция
...
}
Этот synchronized-метод читает байты из сокета. Если read()
блокируется из-за отсутствия доступных байтов, мы хотим, чтобы виртуальный поток размонтировался, освободив платформенный поток для других задач. Однако из-за ключевого слова synchronized виртуальный поток закрепляется за своим носителем (платформенным потоком), блокируя его до завершения операции.
Это может привести к ситуации, когда новые виртуальные потоки не могут быть запущены, поскольку все платформенные потоки либо заняты виртуальными потоками, либо заблокированы в JVM, что негативно сказывается на масштабируемости.
В JDK 24 изменена реализация ключевого слова synchronized: виртуальные потоки смогут монтироваться и демонтироваться внутри метода synchronized. Это позволит продолжать использовать synchronized-методы без ущерба для масштабируемости.
Кроме того, событие jdk.VirtualThreadPinned
в Java Flight Recorder (JFR), которое ранее фиксировалось только при блокировке виртуального потока внутри синхронизированного метода, теперь будет регистрироваться и в других ситуациях, когда виртуальный поток оказывается закреплён за платформенным. Это поможет лучше диагностировать подобные проблемы.
JEP 499: Structured Concurrency (четвёртая предварительная версия)
Structured Concurrency находится в предварительной версии с Java 21. В JDK 24 эта фича остаётся в четвёртый раз без изменений, чтобы собрать больше обратной связи от разработчиков. В Java 22 и Java 23 также отсутствовали изменения. До этого Structured Concurrency существовало в инкубаторе в Java 19 и Java 20.
Традиционные механизмы управления потоками, такие как ExecutorService
и ForkJoinPool
, позволяют запускать асинхронные задачи, но оставляют разработчику ответственность за их контроль и завершение. Это может привести к утечкам ресурсов и сложным сценариям обработки ошибок.
Structured Concurrency предлагает иной подход: все порожденные задачи связаны с родительской областью (TaskScope) и управляются в одном контексте, что гарантирует корректное завершение всех дочерних потоков при завершении родительского.
Structured Concurrency – это подход многопоточного программирования, главная идея которого заключается в следующем: если задача расщепляется на несколько конкурентных подзадач, то эти подзадачи воссоединяются в блоке кода главной задачи. Все подзадачи логически сгруппированы и организованы в иерархию. Каждая подзадача ограничена по времени жизни областью видимости блока кода главной задачи.
Главный класс Structured Concurrency API в пакете java.util.concurrent — StructuredTaskScope.
Выполнение подзадач происходит так:
- Подзадачи ответвляются в отдельные виртуальные потоки методом
fork()
. - Подзадачи воссоединяются методом
join()
в блоке кода главной задачи.
В примере ниже показана задача, которая параллельно запускает две подзадачи и дожидается результата их выполнения:
Response handle() throws ExecutionException, InterruptedException {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> findUser());
Supplier<Integer> order = scope.fork(() -> fetchOrder());
scope.join() // Join both subtasks
.throwIfFailed(); // ... and propagate errors
// Here, both subtasks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
}
У StructuredTaskScope
есть несколько принципиальных отличий, которые делают код безопаснее:
-
Если одна из подзадач,
findUser()
илиfetchOrder()
, завершится с ошибкой, то другая подзадача автоматически прекращает выполнение, если ещё не завершилась. Это поведение контролируется политикой завершения, реализованной вShutdownOnFailure
, но есть и другие политики. -
Если поток, выполняющий
handle()
, прерывается до или во время вызоваjoin()
, то обе подзадачи,findUser()
иfetchOrder()
, автоматически отменяются при выходе из блока. -
Представленный код имеет чёткую структуру: создаются подзадачи, выполняется ожидание их завершения или отмены, после чего принимается решение — либо обработать результаты успешно завершённых задач, либо завершиться с ошибкой (при этом подзадачи уже завершены, и дополнительных действий по их очистке не требуется).
-
В дампе потоков чёткая иерархия задач: потоки, выполняющие
findUser()
иfetchOrder()
, видны как дочерние для родительского потока.
Чтобы попробовать Structured Concurrency, явно включите её.
Structured Concurrency делает многопоточное программирование в Java более управляемым и безопасным. В отличие от традиционных пулов потоков, StructuredTaskScope
гарантирует, что все дочерние задачи корректно завершатся, а ошибки не останутся незамеченными. Этот механизм особенно полезен для управления асинхронными операциями в современных многопоточных приложениях.
Подготовка к ограничению и удалению
JEP 472: Подготовка к ограничению использования JNI
Java Native Interface (JNI) появился в JDK 1.1 и используется для взаимодействия Java-кода с нативным кодом (обычно на C). JNI позволяет Java-коду вызывать нативные функции (downcall) и наоборот (upcall). Однако взаимодействие с нативным кодом несет риски, нарушающие целостность платформы Java.
Основные риски JNI:
- Падение JVM. Ошибки в нативном коде могут привести к аварийному завершению работы JVM.
void Java_pkg_C_setPointerToThree__J(jlong ptr) {
*(int*)ptr = 3;
}
Этот код записывает значение 3 по произвольному адресу, что может вызвать повреждение памяти и падение JVM.
- Работа с небезопасной памятью.
return (*env)->NewDirectByteBuffer(env, 0, 10);
Этот код создает ByteBuffer, привязанный к невалидной области памяти, что приводит к неопределенному поведению.
- Обход проверок доступа.
jclass clazz = (*env)->FindClass(env, "java/lang/String");
jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "[B");
jbyteArray contents = (jbyteArray)(*env)->GetObjectField(env, str, fid);
jbyte b = 0;
(*env)->SetByteArrayRegion(env, contents, 0, 1, &b);
Этот код изменяет содержимое String, хотя строки в Java должны быть неизменяемыми.
- Проблемы с работой GC. Неправильное использование
GetPrimitiveArrayCritical
иGetStringCritical
может вызывать неконтролируемое поведение сборщика мусора.
В JDK 22 появился Foreign Function & Memory API (FFM), который также работает с нативным кодом, но использует механизмы защиты, такие как ограниченные методы. В JDK 24 JNI получает аналогичные ограничения.
При загрузке и линковке нативных библиотек через JNI или FFM API теперь выводится предупреждение. В будущем эти действия могут вызывать исключения по умолчанию.
Включение нативного доступа
Чтобы избежать предупреждений (и в будущем исключений), разработчик должен явно разрешить использование нативного кода:
- Для всего classpath:
java --enable-native-access=ALL-UNNAMED ...
- Для конкретных модулей:
java --enable-native-access=M1,M2,... ...
- Через переменные окружения (
JDK_JAVA_OPTIONS
) или манифест JAR-файла (Enable-Native-Access: ALL-UNNAMED
).
Контроль ограничений нативного доступа
--illegal-native-access=allow
— разрешает доступ без ограничений.--illegal-native-access=warn
— разрешает доступ, но выводит предупреждение (по умолчанию в JDK 24).--illegal-native-access=deny
— выбрасываетIllegalCallerException
при каждом нарушении.
Выявление использования нативного кода
-
JFR события
jdk.NativeLibraryLoad
иjdk.NativeLibraryUnload
отслеживают загрузку и выгрузку библиотек. -
Новый инструмент
jnativescan
анализирует код на предмет использования ограниченных методов и объявлений native-методов.
JDK 24 вводит контроль за использованием JNI для повышения безопасности и целостности Java-платформы. Разработчики должны адаптировать свои приложения, чтобы избежать проблем с будущими версиями JDK, где ограничения станут жестче.
JEP 498: Предупреждение об использовании методов доступа к памяти в sun.misc.Unsafe
Класс sun.misc.Unsafe
долгое время помогал повышать производительность Java-приложений, предоставляя доступ к низкоуровневым операциям. Однако его использование без должных проверок могло привести к обратному эффекту — ухудшению производительности и неожиданным сбоям. Поскольку многие библиотеки использовали sun.misc.Unsafe без достаточных мер безопасности, в Java появились два более надёжных и безопасных API: Variable Handles и Foreign Function & Memory API.
В JDK 23 методы sun.misc.Unsafe
, связанные с доступом к памяти, были помечены как устаревшие. А начиная с JDK 24, согласно JEP 498, JVM будет выводить предупреждения при их использовании.
Эти изменения готовят платформу к полному удалению небезопасных методов sun.misc.Unsafe
в будущих версиях Java.
Заключение
Через полгода выйдет Java 25 LTS. Это значит, что многие фичи, представленные уже в Java 24 будут с нами надолго. А пока можно попробовать новые фичи и оценить, как они повлияют на разработку.