БЛОГ AXIOM JDK
Загрузка...

Что нового в Java 27

Что ждёт нас в JDK 27? Разберём в статье.

12 мин чтения
Что нового в Java 27

В сентябре ожидается выход JDK 27, в которой представлены 9 JEP с новыми и улучшенными фичами. Посмотрим, что нас ждёт в сентябрьском релизе Java.

Чтобы попробовать экспериментальные фичи или фичи в предварительной версии, нужно их явно включить. Вы можете это сделать как через командную строку, так и настроить в интегрированной среде разработки (IDE). Некоторые фичи можно включать на уровне javac, а не только при запуске java.

В командной строке включите фичу в предварительной версии одним из следующих способов:

  • Скомпилируйте программу с помощью javac --release 27 --enable-preview Main.java и запустите ее с помощью java --enable-preview Main.
  • При использовании source code launcher запустите программу с java --enable-preview Main.java.
  • При использовании jshell запустите его с помощью jshell --enable-preview.

JEP 523: Сделать G1 сборщиком мусора по умолчанию во всех окружениях

В JDK 9 (JEP 248) G1 стал сборщиком мусора по умолчанию для серверных окружений. Однако в случаях, когда приложению был доступен один CPU или менее 1792 МБ физической памяти, JVM автоматически выбирала Serial GC.

Раньше это имело смысл: в условиях ограниченных ресурсов Serial GC выигрывал G1 по объёму используемой памяти и пропускной способности. В последние годы G1 сильно доработали (см. JEP 522). Он стал лучше по задержке (latency), объёму используемой нативной памяти (native memory footprint) и пропускной способности, или количеству полезной работы в единицу времени (throughput).

Теперь в JDK 27 (JEP 523), если явно не указать сборщик мусора, то JVM выберет G1 во всех окружениях вне зависимости от числа процессоров и объёма используемой памяти.

Это изменение касается только метрик производительности JVM в условиях ограниченных ресурсов. 

JEP 527: Постквантовый гибридный обмен ключами (hybrid key exchange) для TLS 1.3

JEP 527 добавляет поддержку гибридных алгоритмов обмена ключами для TLS 1.3.

Идея в том, чтобы защититься от сценария «harvest now, decrypt later», при котором злоумышленник записывает зашифрованный трафик сейчас, а расшифровать попытается позже, когда появятся достаточно мощные квантовые компьютеры. Этот сценарий подходит для данных, которые будут актуальны следующие 5-10 лет и более, например, медицинские или персональные данные, финансовые документы и коммерческие контракты.

Гибридная схема обмена ключами предполагает использование двух и более алгоритмов разных групп, так называемых классических алгоритмов и алгоритмов следующего поколения.

Классические алгоритмы

— это алгоритмы, которые сегодня широко используются на практике, но в будущем могут быть признаны устаревшими. В контексте TLS 1.3 это: эллиптический Диффи — Хеллман на кривых secp256r1 или x25519 и Диффи — Хеллман над конечными полями.

Алгоритмы следующего поколения

— это алгоритмы, которые пока не получили широкого распространения, но потенциально могут массово использоваться в будущем. К таким алгоритмам у криптографического сообщества может быть меньше доверия с точки зрения стойкости, потому что они относительно новые или недостаточно хорошо изучены. К этой категории относятся и постквантовые алгоритмы.

Цель такого подхода — получить итоговый ключ сессии, который остаётся безопасным до тех пор, пока хотя бы один из используемых алгоритмов обмена ключами останется не взломан.

JEP добавляет три так называемые гибридные TLS 1.3 named groups, которые комбинируют ECDHE и ML-KEM:

  • X25519MLKEM768       = X25519 + ML-KEM-768
  • SecP256r1MLKEM768    = secp256r1 + ML-KEM-768
  • SecP384r1MLKEM1024   = secp384r1 + ML-KEM-1024

Их имена добавляются в Java Security Standard Algorithm Names, причём названия совпадают с именами IANA.

Если сервер поддерживает гибридный обмен ключами, то Java-клиент сможет использовать его автоматически при условии, что в приложении не ограничены TLS named groups вручную.

Java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ClientExample {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://example.com"))
                .GET()
                .build();

        HttpResponse<String> response =
                client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
    }
}

При переходе на JDK 27 в примере выше в коде ничего менять не нужно.

Однако, если где-то в коде у вас явно заданы named groups, например:

Java
-Djdk.tls.namedGroups="x25519,secp256r1,secp384r1"

То фича работать не будет, так как вы ограничили список named groups только классическими алгоритмами.

В JDK 27 X25519MLKEM768 ставится первым в списке предпочтений TLS 1.3 named groups. Полный порядок см. в JEP 527. Java-клиент сначала предложит гибридную схему X25519MLKEM768, а затем классические варианты. Согласно JEP, X25519MLKEM768 выбран как самый быстрый из гибридных вариантов и именно его сейчас чаще всего включают TLS-клиенты по умолчанию.

Вы также можете выбрать алгоритмы через SSLParameters::setNamedGroups при настройке соединения TLS-сокета:

Java
SSLSocket tlsSock = (SSLSocket)(SSLContext.getDefault().
                                getSocketFactory().createSocket());
SSLParameters params = tlsSock.getSSLParameters();


params.setNamedGroups(new String[] {
    "SecP256r1MLKEM768", "X25519MLKEM768", "secp256r1", "x25519"
});
tlsSock.setSSLParameters(params);

JEP 534: Компактные заголовки объектов по умолчанию

У каждого объекта в Java есть заголовок, который хранит mark word, ссылку на класс и служебные биты JVM.

Идея — уменьшить размер заголовка объекта с 96 до 64 бит на 64-битных архитектурах. Это может снизить потребление кучи и повысить плотность размещения объектов.

Компактные заголовки объектов (Compact Object Headers) появились в JDK 24 (JEP 450) как экспериментальная возможность. Чтобы оценить эффект, нужно было явно включить соответствующий JVM-флаг:

Java
$ java -XX:+UnlockExperimentalVMOptions -XX:+UseCompactObjectHeaders ...

В JDK 25 (JEP 519) компактные заголовки объектов уже перешли в продуктовую возможность. Специальный флаг не требовался, было достаточно следующего:

Java
$ java -XX:+UseCompactObjectHeaders ...

В JDK 27 компактные заголовки объектов становятся поведением по умолчанию в HotSpot JVM на 64-битных архитектурах.

Главный плюс: не нужно переписывать код приложения и вносить какие-либо изменения в поведение объектов. Внутри JVM объект может занимать меньше памяти за счёт более компактного заголовка. Это полезно для приложений, где много маленьких объектов.

Эта фича прошла полный тест-сьют JDK в Oracle, а также протестирована в продакшене на сотнях сервисов Amazon.

В JEP приводятся следующие результаты тестирования на бенчмарках:

  • SPECjbb2015: до 22% меньше использования кучи и до 8% меньше CPU time.
  • SPECjbb2015: до 15% меньше GC с G1 и Parallel GC.
  • JSON parser benchmark: до 10% быстрее.

Если у приложения обнаружатся проблемы при использовании компактных заголовков, то отключите их с помощью флага -XX:-UseCompactObjectHeaders:

Java
$ java -XX:-UseCompactObjectHeaders ...

Для сервисов с большим количеством мелких объектов это одна из самых практичных фич JDK 27.

JEP 536: JFR In-Process Data Redaction

Java Flight Recorder — один из самых полезных инструментов диагностики JVM, который даёт понять, как стартовала JVM, какие были флаги, что происходило с GC, потоками, аллокациями и др.

JFR-записи часто содержат данные о запуске процесса:

  • аргументы командной строки,
  • свойства системы,
  • переменные окружения,
  • параметры JVM,
  • параметры приложения.

В этих данных часто оказываются чувствительные данные, или секреты. Например:

Java
$ export ACCESS_TOKEN=SECRET_TOKEN
$ java -XX:StartFlightRecording:filename=dump.jfr \
       -Xmx2G \
       -Djavax.net.ssl.keyStorePassword=SECRET_PASSWORD \
       -jar application.jar \
      --dbpassword ANOTHER_SECRET_PASSWORD

В старом поведении в файле dump.jfr могли оказаться значения ACCESS_TOKEN, javax.net.ssl.keyStorePassword и --dbpassword.

dump.jfr потом отправляют в поддержку, прикладывают к тикету или кладут в общее хранилище. Так диагностический файл превращается в источник утечки секретов.

JEP 536 решает эту проблему на уровне JVM: секреты редактируются до того, как данные покидают процесс. Теперь JFR по умолчанию будет подставлять [REDACTED] вместо секретов.

Например, чтобы скрыть какие-то переменные окружения или свойства системы под названием confidential или CONFIDENTIAL, а также аргументы командной строки, которые выглядят как URL и содержат логин и пароль (ср. username:password@host), используйте опцию -XX:FlightRecorderOptions и укажите что вы хотите скрыть:

Java
$ export CONFIDENTIAL=SOME_SECRET
$ java -XX:FlightRecorderOptions:'redact-key=confidential,redact-argument=https://*:*@*' \
       -XX:StartFlightRecording:filename=dump.jfr \
       -Dconfidential=ANOTHER_SECRET \
       -jar application.jar https://john:YET_ANOTHER_SECRET@example.com/login --verbose

В примере выше мы хотим, чтобы JFR скрыл значение переменной confidential (см. redact-key=) и аргумент командной строки https://*:*@* (см. redact-argument=) .

Чтобы удостовериться, что данные были скрыты, воспользуйтесь jfr print:

Plain text
$ jfr print \
      --events InitialSystemProperty,JVMInformation,StringFlag,InitialEnvironmentVariable \
      dump.jfr

Java
jdk.JVMInformation {
  startTime = 17:39:02.196 (2026-02-15)
  jvmVersion = "Java HotSpot(TM) 64-Bit Server VM"
  jvmArguments = "-Dconfidential=[REDACTED]
   -XX:FlightRecorderOptions:redact-key=confidential,redact-argument=[REDACTED]
   -XX:StartFlightRecording:filename=dump.jfr"
  jvmFlags = "N/A"
  javaArguments = "-jar application.jar [REDACTED] --verbose"
  jvmStartTime = 17:39:02.050 (2026-02-15)
  pid = 43671
}

jdk.InitialSystemProperty {
  startTime = 17:39:02.196 (2026-02-15)
  key = "confidential"
  value = "[REDACTED]"
}

jdk.InitialSystemProperty {
  startTime = 17:39:02.196 (2026-02-15)
  key = "sun.java.command"
  value = "-jar application.jar [REDACTED] --verbose"
}

jdk.StringFlag {
  startTime = 17:39:02.196 (2026-02-15)
  name = "FlightRecorderOptions"
  value = "redact-key=confidential,redact-argument=[REDACTED]"
  origin = "Command line"
}

jdk.InitialEnvironmentVariable {
  startTime = 17:39:02.244 (2026-02-15)
  key = "CONFIDENTIAL"
  value = "[REDACTED]"
}

По умолчанию JFR будет искать ключи (redact-key) по шаблонам вроде:

Plain text
*api*key*
*auth*
*client*secret*
*credential*
*jaas*config*
*jwt*
*passphrase*
*passwd*
*password*
*private*key*
*pwd*
*secret*
*token*

А аргументы командной строки (redact-argument) по следующим шаблонам:

Plain text
-*api*key *
-*client*secret *
-*credential *
-*jaas*config *
-*jwt *
-*passphrase *
-*passwd *
-*password *
-*private*key *
-*pwd *
-*secret *
-*token *
*api*key*
*client*secret*
*credential*
*jaas*config*
*passphrase*
*passwd*
*password*
*private*key*
*pwd*
*secret*
*token*

Это значит, что типичные случаи закроются без дополнительной настройки.

Если вы укажете свои redact-key/redact-argument, то фильтры по умолчанию не сохраняются автоматически. Чтобы добавить свои фильтры к фильтрам по умолчанию, используется префикс +:

Java
$ java -XX:FlightRecorderOptions:'redact-key=+confidential;secret;@keys.txt' ...

Чтобы не “раздувать” командную строку, фильтры можно загрузить из файла:

Java
java \
  '-XX:FlightRecorderOptions:redact-argument=@args.txt,redact-key=@keys.txt' \
  -XX:StartFlightRecording:filename=dump.jfr \
  -jar app.jar

Если вам по какой-то причине понадобится отключить фичу, то укажите none:

Java
$ java -XX:FlightRecorderOptions:'redact-argument=none,redact-key=none' ...

Помните, если ваше приложение пишет секреты в логи, кастомные JFR-события или в сообщения об исключениях, то JDK не сможет понять весь бизнес-контекст и скрыть все ваши секреты. Секреты не должны попадать в диагностические данные на уровне приложения.

JEP 538: PEM-представление криптографических объектов (третья предварительная версия)

PEM API — API для кодирования криптографических объектов в формат PEM (Privacy-Enhanced Mail) и их декодирования обратно в объекты.

Это API появилось в Java 25 (JEP 470) в предварительной версии, осталось на второе превью в Java 26 (JEP 524) и на третье в Java 27 (JEP 538).

Есть несколько изменений в API:

  • Класс PEM теперь является обычным классом, а не record. В него добавили конструкторы, которые принимают Base64-кодированное содержимое в виде массивов байтов. Для некоторых сценариев это удобнее.

  • Интерфейс DEREncodable переименован в BinaryEncodable, чтобы точнее описывать бинарные данные, которые хранятся в PEM-тексте.

  • В класс EncryptedPrivateKeyInfo добавили методы getKeyPair, которые расшифровывают PKCS#8-кодированный текст, содержащий PublicKey.

  • Методы getKey и getKeyPair класса EncryptedPrivateKeyInfo, которые раньше принимали пароль и Provider, теперь принимают только Key.

  • Метод withFactory PEMDecoder переименован в withFactoriesOf, чтобы лучше отражать, что фабрики ключей и сертификатов получаются из переданного Provider.

  • Добавлен новый класс CryptoException, который сигнализирует об ошибках криптографической обработки во время выполнения.

JEP 531: Lazy Constants (третья предварительная версия)

В Java есть конфликт: final удобно и безопасно, потому что значение задаётся один раз, JVM может ему доверять, а код проще анализировать. Однако final требует ранней инициализации:

Java
class OrderController {

    private final Logger logger = Logger.create(OrderController.class);

    void submitOrder(User user, List<Product> products) {
        logger.info("order started");
        ...
        logger.info("order submitted");
    }

}

Если создание Logger дорогое, то каждый OrderController платит эту цену сразу. Даже если логгер никогда не понадобится.

Раньше инициализацию таких объектов откладывали до последнего, чтобы они создавались только по необходимости. Один из способов — отказаться от final и довериться неизменяемым полям:

Java
class OrderController {

    private Logger logger = null;

    Logger getLogger() {
        if (logger == null) {
            logger = Logger.create(OrderController.class);
        }
        return logger;
    }

    void submitOrder(User user, List<Product> products) {
        getLogger().info("order started");
        ...
        getLogger().info("order submitted");
    }

}

Благодаря Lazy Constants — API для отложенной инициализации неизменяемых значений мы можем переписать пример выше следующим образом:

Java
class OrderController {

    // Прежний вариант:
    // private Logger logger = null;

    // Новый вариант:
    private final LazyConstant<Logger> logger
        = LazyConstant.of(() -> Logger.create(OrderController.class));

    void submitOrder(User user, List<Product> products) {
        logger.get().info("order started");
        ...
        logger.get().info("order submitted");
    }

}

Что важно:

  • Значение вычисляется при первом обращении.
  • Вычисляется не больше одного раза.
  • Корректно работает при конкурентном доступе.
  • После инициализации считается неизменяемым.
  • JVM может оптимизировать доступ ближе к final-семантике.

В JDK 27 (JEP 531) добавлены следующие нововведения:

  • Убрали низкоуровневые методы isInitialized и orElse, чтобы сделать API проще и снизить риск его неправильного использования.
  • Добавить новый factory-метод Set.ofLazy(...), который позволяет задать фиксированный набор возможных элементов, но вычислять вхождение каждого конкретного элемента в итоговое множество только в момент проверки. Благодаря этому у всех трёх базовых типов коллекций появятся “ленивые” версии: List, Set и Map.

Это API находится в предварительной версии. Чтобы воспользоваться фичей, явно включите её.

JEP 533: Structured Concurrency (седьмая предварительная версия)

Structured Concurrency — это возможность, которая должна упростить работу с конкурентным кодом в Java. Идея в том, чтобы рассматривать несколько связанных задач как одну единицу работы: они запускаются вместе, завершаются вместе, а ошибки обрабатываются более предсказуемо. Каждая подзадача запускается в отдельном потоке через fork, а затем все они ожидаются вместе через join. Это полезно в ситуациях, когда в рамках одного запроса нужно параллельно получить несколько данных, например, загрузить пользователя, его заказы и рекомендации.

В однопоточном варианте handle() связь между задачей и подзадачами видна прямо из структуры кода:

Java
Response handle() throws IOException {
    String theUser = findUser();
    int theOrder = fetchOrder();
    return new Response(theUser, theOrder);
}

Здесь fetchOrder() не запускается, пока не завершится findUser(). Если findUser() падает с ошибкой, fetchOrder() вообще не выполняется, а handle() завершается неуспешно.

В однопоточном коде такая иерархия естественно отражается в стеке вызовов: findUser(), а затем fetchOrder() являются подзадачами handle().

С конкурентным кодом было бы проще работать, если бы отношения между родительской задачей и её подзадачами так же явно следовали из структуры кода и сохранялись во время выполнения.

В Java уже есть ForkJoinPool, который задаёт структуру для параллельных задач. Однако он ориентирован прежде всего на вычислительные задачи, а не на I/O-сценарии.

Вот как можно переписать пример выше с использованием Structured Concurrency:

Java
Response handle() throws ExecutionException, InterruptedException {

    try (var scope = StructuredTaskScope.open()) {

        Subtask<String> user = scope.fork(() -> findUser());
        Subtask<Integer> order = scope.fork(() -> fetchOrder());

        scope.join();   // Join subtasks, propagating exceptions

        // Both subtasks have succeeded, so compose their results
        return new Response(user.get(), order.get());

    }

}

StructuredTaskScope ограничивает время жизни подзадач лексической областью — блоком try-with-resources, внутри которого происходят запуск, ожидание, обработка ошибок и сбор результатов.

В отличие от варианта с ExecutorService, здесь жизненный цикл потоков понятен: при любом исходе он ограничен телом try-with-resources.

StructuredTaskScope даёт несколько важных свойств:

  • Если findUser() или fetchOrder() завершается с исключением, вторая подзадача отменяется, если ещё не успела завершиться.
  • Если поток, выполняющий handle(), прервали до или во время join(), обе подзадачи будут автоматически отменены при выходе из scope.
  • Код явно показывает этапы — запустить подзадачи, дождаться завершения или отмены, затем собрать результат или завершиться с ошибкой.
  • Structured Concurrency хорошо ложится на виртуальные потоки (virtual threads). Виртуальные потоки дают много “дешёвых” потоков, а structured concurrency помогает ими правильно управлять.

Structured Concurrency впервые появилась в JDK 19 (JEP 428), а затем в JDK 20 (JEP 437). В JDK 21 (JEP 453) API перешёл в статус превью, при этом метод fork изменили: он стал возвращать Subtask, а не Future. В таком виде API снова выходила в превью в JDK 22 (JEP 462), JDK 23 (JEP 480) и JDK 24 (JEP 499).

Затем Structured Concurrency снова была вынесена на превью в JDK 25 (JEP 505), уже с несколькими изменениями API. Самое заметное из них — публичные конструкторы StructuredTaskScope заменили статическими фабричными методами. В JDK 26 (JEP 525) API снова вышел в превью с несколькими небольшими изменениями.

В JDK 27 предлагается ещё один превью этого API со следующими изменениями:

  • Интерфейсы StructuredTaskScope и Joiner теперь имеют третий параметр типа — R_X, который задаёт тип исключения, выбрасываемого методом join() у StructuredTaskScope.
  • В StructuredTaskScope добавлен новый статический метод open, который реализует политику объединения задач по умолчанию и использует переданный UnaryOperator для формирования конфигурации StructuredTaskScope.
  • Фабричные методы Joiner allSuccessfulOrThrow(), anySuccessfulOrThrow() и awaitAllSuccessfulOrThrow() — теперь создают joiner’ы, из-за которых join() выбрасывает ExecutionException, если результатом выполнения стало исключение. Новые перегрузки этих трёх методов позволяют передать Function, которая создаёт другое исключение.
  • Фабричный метод Joiner.awaitAll() удалён.
  • Метод onTimeout() интерфейса Joiner заменён методом timeout(). Новый метод либо возвращает результат, либо выбрасывает исключение, если scope был отменён из-за таймаута. Если timeout() выбрасывает исключение, оно выбрасывается с CancelledByTimeoutException в качестве причины.

JEP 537: Vector API (двенадцатый инкубатор)

Vector API появился в JDK 16 (JEP 388) в модуле jdk.incubator.vector и остаётся в инкубационном периоде. В JEP 537 API переходит без изменений. Только встроенную библиотеку SLEEF, которая используется для векторных математических интринсик на ARM и RISC-V, обновили с версии 3.6.1 до 3.9.0.

Vector API нужен для векторных вычислений, которые во время выполнения компилируются в оптимальные SIMD-инструкции на конкретной платформе.

Эта возможность будет полезна в разных областях: машинном обучении, линейной алгебре, криптографии, финансах, а также во внутреннем коде самого JDK.

Vector API останется в инкубаторе тех пор, пока необходимые возможности Project Valhalla не будут доступны в превью. Как только это произойдет, Vector API перейдёт из инкубатора в статус превью.

JEP 532: Примитивные типы в паттернах, instanceof и switch (пятая предварительная версия)

Примитивные типы в паттернах, instanceof и switch были представлены в JDK 23 (JEP 455) в предварительной версии, остались в предварительной версии без изменений в JDK 24 (JEP 488), JDK 25 (JEP 507) и JDK 26 (JEP 530). В JDK 27 (JEP 532) предлагается в пятый раз выпустить фичу в предварительной версии без изменений.

Долгое время паттерны в основном работали со ссылочными типами. Теперь же с примитивными типами можно работать в паттернах, instanceof и switch.

Это делает проверки чисел и преобразования типов более выразительными и безопасными. Подробнее эту фичу мы разбирали в статье Что нового в Java 26.

Это API находится в предварительной версии. Чтобы воспользоваться фичей, явно включите её.

JDK 27 — не LTS, поэтому массового production-перехода ждать не стоит. Этот релиз полезен для раннего тестирования и подготовки к следующей LTS-версии.

Axiom JDK 27 будет доступен для скачивания в личном кабинете после его официального релиза.

Александра Бикбаева

Александра Бикбаева

DevRel Axiom JDK

Похожие статьи

Java-разработка

Что нового в Java 26

Александра Бикбаева

Что нового в Java 26

В JDK 26 доступны 10 JEP, охватывающих язык, библиотеки, многопоточность, производительность и сборку мусора. Скачать Ax...

Будьте в курсе мира Java

Релизы, патчи безопасности и советы для разработчиков — без лишнего шума