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

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

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

В этой заключительной части мы рассмотрим оставшиеся шесть фичей, среди которых примитивные типы в patterns, instanceof и switch, compact source files and instance main methods и другие.

Содержание

Категории JEP-ов

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

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

Эта фича была представлена в JDK 21 (JEP 445), затем улучшена в JDK 22 (JEP 463), JDK 23 (JEP 477), JDK 24 (JEP 495) и утвердилась в JDK 25 c рядом изменений:

  • Новый класс IO для базового консольного ввода-вывода теперь находится в пакете java.lang, а не java.io. Следовательно, он неявно импортируется каждым исходным файлом.

  • Статические методы класса IO больше не импортируются неявно в компактных исходных файлах. Поэтому вызовы этих методов должны указывать имя класса, например IO.println("Hello, world!"), если только методы не импортированы явно.

  • Реализация класса IO теперь основана на System.out и System.in, а не на классе java.io.Console.

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

Однако Java задумывалась и как первый язык для обучения. Начинающие программисты обычно пишут маленькие отдельные программы. Им не нужны инкапсуляция или пространства имён. В Java надо было написать довольно большое количество Java-специфичного кода, чтобы написать простой Hello, World. Из-за всех import и прочих final, static, void, main начинающему программисту нужно было долго добираться до цикла for и оператора if. Концепции классов, пакетов и модулей в этот момент только мешают.

Классический пример «Hello, World!» оказывается перегружен:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Здесь слишком много всего для начинающего:

  • Ключевое слово public и объявление класса нужны для программирования в большом, но бесполезны в таком примере.
  • Параметр String[] args тоже — он связывает программу с внешними компонентами (оболочкой ОС), хотя здесь не используется.
  • Модификатор static не только загадочен для новичков, но и навязывает плохую практику: все методы приходится делать статическими, пока студент не освоит работу с объектами.
  • Вызов System.out.println выглядит как магия, вместо простой функции.

В итоге новичку приходится сталкиваться со сложными концепциями в самом начале знакомства. Это затрудняет обучение и создаёт впечатление, что Java слишком сложна для изучения и понимания.

Цель этого JEP — выстроить обучение в правильном порядке: от простых концепций программирования (ввод-вывод, массивы, циклы) к более сложным (объекты, классы).

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

Теперь метод main() может быть нестатическим, без модификатора public и без параметра String[] args:

class HelloWorld {
    void main() {
        System.out.println("Hello, World!");
    }
}

Можно писать код напрямую, без объявления класса:

void main() {
    System.out.println("Hello, World!");
}

В пакете java.lang появился класс IO с простыми методами для строкового ввода/вывода:

void main() {
    IO.println("Hello, World!");
}

В классе java.lang.IO будут следующие методы:

public static void print(Object obj);
public static void println(Object obj);
public static void println();
public static String readln(String prompt);
public static String readln();

В компактных исходных файлах автоматически импортируются все публичные классы и интерфейсы модуля java.base. Это значит, что List, Math, Path и другие часто используемые классы доступны без import.

Пример с вводом:

void main() {
    String name = IO.readln("Введите имя: ");
    IO.print("Приятно познакомиться, ");
    IO.println(name);
}

Компактный исходный файл легко превратить в обычный: достаточно обернуть поля и методы в класс и добавить import.

// Было:
void main() {
    var names = List.of("James", "Bill", "Guy");
    for (var n : names) IO.println(n);
}

// Стало:
import java.util.List;

class Example {
    void main() {
        var names = List.of("James", "Bill", "Guy");
        for (var n : names) IO.println(n);
    }
}

Компактные исходные файлы и упрощённый main() облегчают старт работы с Java, сокращают нагромождённый сложными конструкциями код и делают язык более доступным для новичков. Java становится удобнее для написания маленьких утилит в совокупности с возможностью запуска java-файлов (а не только class-файлов), не ломая существующую модель платформы Java.

JEP 502: Stable Values (предварительная версия)

В JDK 25 предлагается ввести API для стабильных значений — объектов, которые хранят неизменяемые данные. Стабильные значения рассматриваются JVM как константы, что позволяет применять те же оптимизации производительности, что и при объявлении поля final. По сравнению с полями final, стабильные значения дают большую гибкость в отношении времени их инициализации.

Большинство Java-разработчиков знакомы с советом «предпочитайте неизменяемость» или «минимизируйте изменяемость» (Effective Java, 3-е издание, пункт 17).
Неизменяемость даёт множество преимуществ: объект может находиться только в одном состоянии, а значит, может безопасно использоваться одновременно в разных потоках.

Главный инструмент платформы Java для обеспечения неизменяемости — это поля с модификатором final (хотя final не такой уж и неизменяемый). Но у final есть ограничения:

  • поля final у экземпляра должны быть инициализированы в конструкторе,
  • статические — в инициализаторе класса,
  • порядок инициализации строго определяется порядком объявления в исходном коде.

Эти ограничения мешают использовать final в реальных приложениях.

Рассмотрим неизменяемость на практике. Ниже представлен компонент, который пишет события в логгер:

class OrderController {
    private final Logger logger = Logger.getLogger(OrderController.class);

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

Так как logger объявлен как final, он обязан инициализироваться сразу при создании OrderController. Но получение логгера может быть дорогой операцией (например, парсинг конфигурации, подготовка хранилища).

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

class Application {
    static final OrderController   ORDERS   = new OrderController();
    static final ProductRepository PRODUCTS = new ProductRepository();
    static final UserService       USERS    = new UserService();
}

Многие логгеры могут вообще не понадобиться, но всё равно будут инициализированы.

Чтобы отложить инициализацию, разработчики часто отказываются от final и применяют «ленивый» подход с изменяемыми полями:

class OrderController {
    private Logger logger = null;

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

Теперь логгер создаётся только при первом обращении. Но появляются недостатки:

  • Требуется следить, чтобы доступ к полю происходил только через getLogger, иначе возможен NullPointerException.
  • В многопоточной среде возможны гонки и создание нескольких логгеров.

JIT не может оптимизировать доступ так, как с final.

К вопросу об отложенной неизменяемости. Сегодня у нас есть два крайних варианта:

  • final — слишком жёсткий, всё инициализируется сразу;
  • non-final — слишком свободный, теряется гарантия корректности и оптимизации.

Нужен промежуточный вариант — механизм, позволяющий отложить инициализацию, но при этом гарантировать неизменяемость после первого присвоения.

Для этого ввели Stable Value — это объект (StableValue<T>), который хранит одно значение данных:

  • изначально оно не установлено,
  • должно быть инициализировано до первого использования,
  • после инициализации становится неизменяемым.

Таким образом, StableValue реализует отложенную неизменяемость.

Пример:

class OrderController {
    private final StableValue<Logger> logger = StableValue.of();

    Logger getLogger() {
        return logger.orElseSet(() -> Logger.create(OrderController.class));
    }
}

Метод orElseSet либо возвращает уже установленное значение, либо инициализирует его через переданный лямбда-выражение и гарантирует, что оно будет вызвано ровно один раз, даже при конкурентных вызовах.

Stable Value API доступен в предварительной версии. Чтобы использовать его, явно включите.

Сравнение с другими подходами

Вариант Количество обновлений Где инициализируется Константные оптимизации Потокобезопасность
final 1 Конструктор/статический блок Да Да
StableValue 0 или 1 В любом месте Да, после установки Да, гарантировано
non-final 0 … ∞ В любом месте Нет Нет

Stable Supplier — ленивый поставщик значения:

private final Supplier<Logger> logger =
    StableValue.supplier(() -> Logger.create(OrderController.class));

Теперь доступ к логгеру выглядит как logger.get(), без отдельного метода-обёртки.

Stable List — список, элементы которого инициализируются лениво и независимо друг от друга. Это удобно для пулов объектов.

Под капотом Stable Value хранит данные в non-final поле, аннотированном внутренней аннотацией JDK @Stable. JVM доверяет, что значение поля изменится не более одного раза, и может применять оптимизации вроде constant folding.

Stable Value закрывает пробел между final и non-final, позволяя откладывать инициализацию до момента первого использования, сохраняя при этом преимущества неизменяемости и производительности.

JEP 470: PEM Encodings of Cryptographic Objects (предварительная версия)

Java API уже давно поддерживает криптографические объекты: приватные и публичные ключи, сертификаты, CRL. Эти объекты используются при подписании и проверке подписей, валидации TLS-соединений и в других криптографических операциях.

Во многих случаях приложения обмениваются этими объектами через UI, сеть, хранилища. На практике для этого часто используется PEM (Privacy-Enhanced Mail), определённый в RFC 7468.

PEM — это текстовый формат, придуманный для отправки криптографических объектов по e-mail. С течением времени он расширился и стал использоваться в других целях. Центры сертификации выдают цепочки сертификатов в PEM. OpenSSL и подобные библиотеки поддерживают операции с PEM. OpenSSH хранит ключи в PEM. Аппаратные токены (например, Yubikey) принимают и отдают объекты в PEM.

Пример PEM-записи (публичный ключ на эллиптических кривых):

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEi/kRGOL7wCPTN4KJ2ppeSt5UYB6u
cPjjuKDtFTXbguOIFDdZ65O/8HTUqS/sVzRF+dg7H3/tkQ/36KdtuADbwQ==
-----END PUBLIC KEY-----

Формат прост: Base64-представление бинарного объекта, обёрнутое в заголовок BEGIN и футер END, где также указывается тип объекта, например, PUBLIC KEY.

Сегодня в Java нет простого API для работы с PEM:

  • Кодирование ключа в PEM — рутинно, но несложно.
  • Декодирование требует парсинга, выбора нужной фабрики (KeyFactory/CertificateFactory) и знания алгоритма.
  • Работа с зашифрованными ключами требует десятков строк кода.

Предложенный API должен это упростить.

В пакет java.security добавляются новые элементы:

  • Интерфейс DEREncodable — маркер для криптографических объектов, которые можно преобразовать в DER (бинарный формат) и обратно.
public sealed interface DEREncodable
    permits AsymmetricKey, KeyPair,
            PKCS8EncodedKeySpec, X509EncodedKeySpec,
            EncryptedPrivateKeyInfo, X509Certificate, X509CRL,
            PEMRecord { }
  • Классы PEMEncoder и PEMDecoder — для кодирования и декодирования PEM соответственно. Экземпляры этих классов неизменяемые (immutable), потокобезопасные, переиспользуемые.

  • Класс PEMRecord — реализует DEREncodable, служит для PEM-данных без стандартного Java API (например, запросы PKCS#10).

Это API в предварительной версии, включается оно флагом --enable-preview.

PEMEncoder предоставляет методы:

public final class PEMEncoder {
    public static PEMEncoder of();

    public byte[] encode(DEREncodable so);
    public String encodeToString(DEREncodable so);

    public PEMEncoder withEncryption(char[] password);
}

Пример:

PEMEncoder pe = PEMEncoder.of();
String pem = pe.encodeToString(new KeyPair(publicKey, privateKey));

Если кодируется приватный ключ — можно зашифровать:

String pem = pe.withEncryption(password).encodeToString(privateKey);

PEMDecoder предоставляет методы:

public final class PEMDecoder {
    public static PEMDecoder of();

    public DEREncodable decode(String str);
    public <S extends DEREncodable> S decode(String str, Class<S> cl);

    public PEMDecoder withDecryption(char[] password);
    public PEMDecoder withFactory(Provider provider);
}

Пример:

PEMDecoder pd = PEMDecoder.of();
switch (pd.decode(pem)) {
    case PublicKey pub -> ...
    case PrivateKey priv -> ...
    default -> throw new IllegalArgumentException();
}

Если тип известен заранее:

ECPublicKey key = pd.decode(pem, ECPublicKey.class);

Для зашифрованных ключей:

ECPrivateKey eckey = pd.withDecryption(password)
                       .decode(pem, ECPrivateKey.class);

PEMRecord позволяет работать с PEM-текстами, для которых нет стандартного Java API:

public record PEMRecord(String type, String content, byte[] leadingData)
    implements DEREncodable { ... }

В классе EncryptedPrivateKeyInfo есть методы для удобного шифрования/дешифрования приватных ключей:

EncryptedPrivateKeyInfo epki =
    EncryptedPrivateKeyInfo.encryptKey(privateKey, password);

byte[] pem = PEMEncoder.of().encode(epki);

PrivateKey key = epki.getKey(password);

Предложенный API для работы с PEM-файлами:

  • Упрощает интеграцию с PKI и системами безопасности.
  • Устраняет необходимость в ручном парсинге Base64 и ошибок форматирования.
  • Обеспечивает безопасное хранение шифруемых ключей.

JEP 509: JFR CPU-Time Profiling (экспериментальная возможность)

В JDK 25 JDK Flight Recorder (JFR) будет собирать более точную информацию о профилировании CPU-времени на Linux. Эта возможность представлена как экспериментальная.

Запущенная программа потребляет вычислительные ресурсы — память, процессорное время (CPU cycles) и реальное время выполнения. Профилирование — это измерение потребления этих ресурсов конкретными элементами программы. Например, профиль может показать, что один метод потребляет 20% ресурсов, а другой — лишь 0.1%.

Профилирование помогает разработчикам оптимизировать именно те места, которые реально влияют на производительность. Без него можно тратить усилия на ускорение метода, который и так почти не потребляет ресурсов. Например, если метод занимает 0.1% времени выполнения программы, то ускорение его в 10 раз сократит общее время лишь на 0.09%.

JFR (JDK Flight Recorder) — это встроенный инструмент профилирования и мониторинга в JDK. Он с низкими накладными расходами записывает события, генерируемые JVM и программным кодом.

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

JFR хорошо поддерживает профилирование памяти в куче, но возможности профилирования CPU всё ещё ограничены.

Сегодня для CPU-профилей используется execution sampler: он с определённым интервалом (например, каждые 20 мс) снимает стек активных Java-потоков. Это работает на всех ОС, но имеет ограничения:

  • учитываются только Java-методы, но не нативный код,
  • часть сэмплов может пропадать без учёта в статистике,
  • выборка ограничивается подмножеством потоков.

В итоге профиль может быть неточным, особенно при коротких интервалах наблюдения.

В Linux начиная с ядра 2.6.12 есть встроенный таймер, который генерирует сигналы через фиксированные интервалы CPU-времени, а не реального времени. Большинство профилировщиков под Linux используют именно его. Популярные инструменты (например, async-profiler) применяют этот механизм для Java-программ, но вынуждены взаимодействовать с JVM через непубличные API, что небезопасно и может приводить к сбоям.

Решение: встроить поддержку CPU-time профилирования в JFR, используя Linux CPU timer. Это позволит безопасно и точно измерять потребление CPU даже при выполнении нативного кода.

В JDK 25 добавляется новый тип событий JFR — jdk.CPUTimeSample, доступный только на Linux. Сэмплирование выполняется по CPU-времени каждого потока. События execution sampler (jdk.ExecutionSample) остаются независимыми и могут использоваться параллельно.

Включение при запуске:

java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,filename=profile.jfr ...

Рассмотрим пример. У нас есть программа с двумя потоками:

  • tenFastRequests — 10 быстрых HTTP-запросов (по 10 мс каждый).
  • oneSlowRequest — 1 медленный запрос (100 мс).

Общее время работы сопоставимо, но tenFastRequests использует значительно больше CPU. Новый профиль JFR показывает это отличие (например, на flame graph в JMC).

Новый профиль JFR

jdk.CPUTimeSample содержит:

  • startTime — CPU-время перед снятием стека,
  • eventThread — поток,
  • stacktrace — стек вызовов (или null, если снятие не удалось),
  • samplingPeriod — период семплирования,
  • biased — смещён ли сэмпл из-за safepoint.

Частота задаётся через throttle:

  • как период: throttle=10ms — выборка каждые 10 мс CPU-времени на поток,
  • как общая скорость: throttle=500/s — 500 сэмплов в секунду на все потоки.
  • по умолчанию 500/s (или 10ms в profile.jfc).

Дополнительно введено событие jdk.CPUTimeSamplesLost, чтобы учитывать потерянные сэмплы.

Так как это экспериментальная возможность, события помечены аннотацией @Experimental, но не требуют включения -XX:+UnlockExperimentalVMOptions.

Можно использовать непрерывное профилирование (continuous profiling), если выбрать достаточно низкую частоту.

Или запускать профилирование по требованию:

jfr configure --input profile.jfc --output /tmp/cpu_profile.jfc \
  jdk.CPUTimeSample#enabled=true jdk.CPUTimeSample#throttle=20ms

jcmd <pid> JFR.start settings=/tmp/cpu_profile.jfc duration=4m

Запись можно остановить вручную через jcmd <pid> JFR.stop.

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

Эта новая возможность использует механизм cooperative sampling, добавленный в JEP 518.

JEP 518: JFR Cooperative Sampling

В JDK 25 повысится стабильность JDK Flight Recorder (JFR) при асинхронном семплировании стека Java-потоков. Для этого трассировка вызовов будет выполняться только в safepoint-ах, с минимизацией смещения (safepoint bias).

Запущенная программа потребляет ресурсы — память, процессорное время (CPU cycles) и время выполнения. Профилирование измеряет потребление этих ресурсов отдельными элементами программы. Например, профиль может показать, что один метод потребляет 20% ресурсов, а другой лишь 0.1%.

Профилирование помогает разработчикам находить узкие места и оптимизировать код там, где это действительно важно. Без него можно потратить усилия на ускорение метода, который почти не влияет на общую производительность. Например, если метод занимает всего 0.1% времени выполнения, его ускорение в 10 раз сократит общее время лишь на 0.09%.

JFR — это встроенное в JDK средство профилирования и мониторинга. Его ядро — это малозатратный механизм записи событий, которые генерирует JVM или сам код программы.

Часть событий фиксируется при каждом действии (например, загрузка класса).

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

JFR может строить профиль execution-time (по реальному времени выполнения, wall-clock time), снимая стеки потоков с фиксированным интервалом, например каждые 20 мс. Каждый сэмпл записывается как JFR-событие с трассировкой стека. Инструменты вроде jfr или JDK Mission Control (JMC) умеют визуализировать эти данные в виде текстовых и графических профилей.

Чтобы получить трассировку стека, поток-сэмплер JFR должен приостановить целевой поток и разобрать его кадры вызовов. HotSpot JVM хранит метаданные для разбора стека, но они гарантированно корректны только в специальных точках кода — safepoint-ах.

Если сэмплировать стеки только в safepoint-ах, возникает проблема safepoint bias: часто выполняющийся участок кода может находиться далеко от safepoint-а, и профиль потеряет точность. Эта проблема хорошо известна и подробно исследована.

Чтобы избежать safepoint bias, JFR сегодня семплирует стеки асинхронно, то есть может приостановить поток в произвольном месте кода. Но в таких точках метаданные для разбора стека могут быть некорректными, и JFR приходится использовать эвристики.

К сожалению, эти эвристики неэффективны, а при ошибках могут приводить к сбоям JVM.

JFR пытается защититься через платформозависимые механизмы crash protection, но они ненадёжны при конкурирующих событиях вроде выгрузки классов.

Переработан механизм сэмплирования JFR, чтобы отказаться от рискованных эвристик. Теперь разбор стека выполняется только в safepoint-ах. Чтобы минимизировать safepoint bias, используется кооперативное семплирование:

  1. Когда приходит время сэмпла, поток-сэмплер JFR приостанавливает целевой поток.
  2. Вместо разбора стека он лишь фиксирует его program counter и stack pointer в запросе и сохраняет его в локальную очередь.
  3. Затем он организует остановку целевого потока в ближайшем safepoint и возобновляет выполнение.
  4. При достижении safepoint-а служебный код проверяет очередь запросов. Для каждого запроса он восстанавливает стек, корректируя смещение, и создаёт JFR-событие.

Среди преимуществ подхода можно выделить:

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

Метод работает корректно для Java-кода (интерпретируемого и скомпилированного). Для нативного кода сохранён старый подход.

Новый механизм не полностью исключает safepoint bias. В некоторых случаях (например, при сэмплировании метода с intrinsic-реализацией) стек может быть недоступен. Тогда в трассировку попадёт последний доступный Java-фрейм. Эта проблема планируется к решению в будущем.

Механизм, предложенный в рамках этого JEP, используется в реализации JEP 509 (JFR CPU-Time Profiling).

JEP 520: JFR Method Timing & Tracing

В JDK 25 добавили расширение JDK Flight Recorder для тайминга и трассировки методов с помощью инструментации байткода.

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

Примеры:

  • Если приложение слишком долго стартует, трассировка статических инициализаторов (<clinit>) может показать, какие загрузки классов можно отложить.
  • Если метод был переписан ради исправления производительной ошибки, измерение его времени выполнения подтвердит успешность фикса.
  • Если приложение падает из-за исчерпания пула соединений с БД, трассировка методов открытия соединений подскажет, как эффективнее управлять ресурсами.

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

  • JMH позволяет замерять время выполнения методов,
  • отладчики на базе JDI ставят брейкпоинты и анализируют стеки.

Но в тестировании и продакшене всё сложнее:

  • Временные логи или кастомные JFR-события неудобны, особенно для библиотек и кода JDK.
  • Выборочные профайлеры (включая встроенный в JFR и async-profiler) дают только стеки часто выполняемых методов, но не покрывают каждый вызов.
  • Java-агенты решают проблему, но встроенные в JVM инструменты были бы быстрее и проще.

Вывод: JDK должен предоставлять встроенный способ низконакладного тайминга и трассировки методов.

В JFR добавляются два новых события:

  • jdk.MethodTiming — считает количество вызовов и среднее время выполнения.
  • jdk.MethodTrace — фиксирует время выполнения, стек и поток вызова.

Оба события принимают фильтр для выбора методов.

Пример: чтобы посмотреть, что вызывает HashMap::resize, можно задать фильтр:

$ java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr ...
$ jfr print --events jdk.MethodTrace --stack-depth 20 recording.jfr

Фильтр задаётся так же, как method reference в исходном коде: java.util.HashMap::resize. JVM при старте внедрит байткод для генерации события MethodTrace.

На практике JFR настраивают через файлы (default.jfc, profile.jfc). Теперь у них появятся новые опции:

  • method-timing — считает количество вызовов и среднее время;
  • method-trace — пишет стек вызова.

Пример: замер всех статических инициализаторов.

$ java '-XX:StartFlightRecording:method-timing=::<clinit>,filename=clinit.jfr' ...
$ jfr view method-timing clinit.jfr

Фильтр может задавать:

  • конкретный метод,
  • класс (все методы внутри),
  • аннотацию (все методы с ней или в классах с ней).

Можно комбинировать несколько фильтров через точку с запятой (;).

Можно создать свои аннотации (например, @StopWatch, @Initialization, @Debug) и включать тайминг/трассировку по ним:

$ java -XX:StartFlightRecording:method-trace=@com.example.Debug ...

С помощью JMX и RemoteRecordingStream можно настраивать тайминг и трассировку по сети, включая фильтрацию и периодическую агрегацию результатов. При остановке записи JVM удаляет внедрённый байткод и восстанавливает исходное состояние.

Детали событий:

  • jdk.MethodTiming: метод, время начала, количество вызовов, min/avg/max wall-clock время.
  • jdk.MethodTrace: метод, время начала, длительность, поток, стек.

Точность avg и duration зависит от стоимости получения timestamp на конкретном железе и ОС.

Заключение

Фичи JDK 25 направлены на развитие языка в нескольких направлениях: спецификация, многопоточность, производительность и рантайм и профилирование.

  • Compact Source Files и новые правила для main() снижают порог входа в язык. Теперь проще учить Java, писать скрипт,, не отвлекаясь на избыточный синтаксис.
  • Structured Concurrency делает работу с многопоточностью более предсказуемой и безопасной, а улучшения в JFR позволяют глубже и надёжнее профилировать приложения без риска крашей и с меньшими накладными расходами.
  • Криптографические улучшения (встроенный PEM API, новые KDF API) закрывают реальную боль: работу с ключами и сертификатами теперь можно делать безопаснее и проще, без использования сторонних библиотек.

Таким образом, новичкам станет легче “вкатываться” в Java, а у профессионалов будет больше инструментов “из коробки” для разработки надёжных и производительных систем.

Author image

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

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

Axiom JDK info@axiomjdk.ru Axiom JDK logo Axiom JDK На страже безопасности Java 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