Что нового в Java 24. Часть 3

Что нового в Java 24. Часть 3

Это третья и заключительная часть обзора JEP в Java 24. Если вы пропустили первые две части, см. Что нового в Java. Часть 1 и Что нового в Java. Часть 2.

В этой части мы рассмотрим оставшиеся восемь фичей, среди которых долгожданный Project Leyden, Structured Concurrency, Key Derivation Function API и другие.

Содержание

Как начать использовать фичи в предварительной и экспериментальной версиях

Чтобы попробовать экспериментальные фичи или фичи в предварительной версии, нужно их явно включить. Вы можете это сделать как через командную строку, так и настроить в интегрированной среде разработки (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 выполняет две основные операции:

  1. Создание и инициализация — создание объекта KDF и его настройка с нужными параметрами.

Класс KDF предоставляет стандартные методы getInstance(), которые принимают имя алгоритма, а также, при необходимости, параметры KDF (KDFParameters) и криптопровайдера (Provider). В рамках этого JEP реализован только алгоритм HMAC-based Extract-and-Expand Key Derivation Function (HKDF).

Пример:

KDF hkdf = KDF.getInstance("HKDF-SHA256");
  1. Генерация ключей — использование заданного секретного значения и дополнительных параметров, включая параметры описания выходного значения, для получения производного ключа или данных.

Процесс генерации ключей схож с хешированием паролей: 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-кэша:

  1. Первая стадия (“обучение”): запуск приложения с записью AOT-конфигурации.

  2. Вторая стадия: создание кэша на основе AOT-конфигурации.

  3. Третья стадия: использование 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 включает три основных функции:

  1. Генерация ключевой пары: создаёт открытый и закрытый ключи.
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-ключи.

  1. Инкапсуляция ключа (используется отправителем): принимает открытый ключ получателя и возвращает секретный ключ 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();         // Секретный ключ
  1. Декапсуляция ключа (используется получателем): принимает закрытый ключ и сообщение инкапсуляции и извлекает секретный ключ 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.concurrentStructuredTaskScope.

Выполнение подзадач происходит так:

  1. Подзадачи ответвляются в отдельные виртуальные потоки методом fork().
  2. Подзадачи воссоединяются методом 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 будут с нами надолго. А пока можно попробовать новые фичи и оценить, как они повлияют на разработку.

Author image

Сергей Лунегов

Директор по продуктам Axiom JDK

Axiom JDK info@axiomjdk.ru Axiom JDK logo Axiom Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 Axiom JDK 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67