Большинство статей о нововведениях Java 25 посвящены тому, что разработчики получают после обновления: Scoped Values, более гибкие тела конструкторов, компактные исходные файлы, Stream Gatherers. Но в этой статье речь пойдёт о другой стороне перехода между LTS‑версиями — о том, что после обновления перестаёт работать.
Между Java 21 и Java 25 вышло четыре версии платформы. Некоторые возможности, объявленные устаревшими в Java 21, к Java 25 были окончательно удалены. Одни изменения приводят к явным сбоям уже при запуске приложения. Другие, что гораздо хуже, проявляются незаметно: сборка проходит успешно, приложение запускается, а ошибка обнаруживается лишь неделю спустя.
Все приведённые ниже примеры были скомпилированы и запущены на JDK 21.0.7 и JDK 25.0.3. Результаты соответствуют фактическому выводу обеих JVM. Я лишь сократил пути к файлам и убрал временные метки из журналов. В каждом разделе есть ссылки на соответствующие JEP или примечания к выпуску.
1. Процессоры аннотаций перестают запускаться — и никто об этом не сообщает
Именно эта проблема с наибольшей вероятностью затронет типичный Maven‑проект, поскольку возникает без каких‑либо сообщений об ошибках.
До JDK 22 включительно компилятор javac искал процессоры аннотаций в пути к классам (classpath) и автоматически запускал все обнаруженные. Начиная с JDK 23 такое поведение по умолчанию отключено: процессоры запускаются только при явной настройке (-processor, --processor-path, --processor-module-path) либо при указании -proc:full.
Чтобы продемонстрировать изменение, я создал процессор, который ничего не делает, кроме вывода информационного сообщения, упаковал его в JAR‑файл с записью в META‑INF/services и скомпилировал обычный класс, добавив этот JAR в classpath:
javac -cp hello-processor.jar -d out App.java
На JDK 21 процессор запускается, а javac предупреждает о предстоящем изменении поведения:
Note: Annotation processing is enabled because one or more processors were found on the class path. A future release of javac may disable annotation processing unless at least one processor is specified by name (-processor), or a search path is specified (--processor-path, --processor-module-path), or annotation processing is enabled explicitly (-proc:only, -proc:full). Use -Xlint:-options to suppress this message. Use -proc:none to disable annotation processing. Note: HelloProcessor ran
На JDK 25 та же команда вообще ничего не выводит и завершается с кодом 0. Процессор попросту не запускается.
В случае с Lombok это приведёт к ошибкам cannot find symbol для всех геттеров, которые он должен был сгенерировать. Такую проблему вы хотя бы заметите. Но если процессор создаёт необязательные артефакты — например, файлы метаданных, реестр сервисов или реализации мапперов, обнаруживаемые через рефлексию, — ошибка может остаться незамеченной вплоть до выполнения приложения.
Чтобы исправить ситуацию, необходимо явно объявить процессоры аннотаций. В Maven их следует перечислить в annotationProcessorPaths конфигурации maven‑compiler‑plugin. Альтернативный вариант — установить <proc>full</proc> (пользовательское свойство maven.compiler.proc), чтобы восстановить прежнее поведение в версии плагина, которая поддерживает эту настройку.
Gradle ещё с версии 5.0 использует отдельную конфигурацию annotationProcessor, поэтому сборки Gradle обычно не затрагиваются этим изменением.
Если в журнале сборки на JDK 21 присутствует предупреждение Annotation processing is enabled because…, значит, проблема касается и вашего проекта.
2. Security Manager окончательно отключён, а один параметр не даёт JVM запуститься
Механизм Security Manager был объявлен устаревшим с перспективой удаления ещё в Java 17. В JDK 24 предложение JEP 486 окончательно отключило его: API по‑прежнему существует, но активировать Security Manager больше невозможно.
Рассмотрим следующий класс:
public class Sm { public static void main(String[] args) { System.out.println("security manager: " + System.getSecurityManager()); try { System.setSecurityManager(new SecurityManager()); System.out.println("installed: " + System.getSecurityManager()); } catch (UnsupportedOperationException e) { System.out.println(e); } } }
На JDK 21 при использовании параметра -Djava.security.manager=allow Security Manager успешно устанавливается, хотя система и выдаёт предупреждение об устаревании.
На JDK 25 JVM даже не доходит до выполнения метода main:
$ java -Djava.security.manager=allow Sm Error occurred during initialization of VM java.lang.Error: A command line option has attempted to allow or enable the Security Manager. Enabling a Security Manager is not supported. at java.lang.System.initPhase3(java.base@25.0.3/System.java:1970)
Если убрать этот параметр, вызов setSecurityManager теперь выбрасывает UnsupportedOperationException с сообщением Setting a Security Manager is not supported.
Поэтому проверьте стартовые скрипты, Dockerfile, конфигурации systemd и настройки запуска в IDE на наличие java.security.manager. Единственное допустимое значение теперь — -Djava.security.manager=disallow.
Но есть ещё одна особенность, способная затронуть даже тех, кто никогда не использовал Security Manager напрямую. Речь о методе Subject.getSubject(AccessControlContext). Он по‑прежнему встречается в старом коде аутентификации, интеграциях с Kerberos и некоторых библиотеках эпохи Hadoop:
System.out.println(Subject.getSubject(AccessController.getContext()));
На JDK 21 при отсутствии субъекта этот код выводит null.
На JDK 25 результат будет другим:
Exception in thread "main" java.lang.UnsupportedOperationException: getSubject is not supported at java.base/javax.security.auth.Subject.getSubject(Subject.java:277)
Методы для замены — Subject.current() и Subject.callAs(...) — доступны начиная с Java 18. Поэтому мигрировать на них можно ещё до перехода с Java 21.
3. Удалённые методы Thread: обработчик исключения не поможет
Методы Thread.suspend(), Thread.resume(), ThreadGroup.suspend(), ThreadGroup.resume() и ThreadGroup.stop() были удалены в JDK 23, а Thread.countStackFrames() — в JDK 22.
В Java 21 они уже не выполняли никаких полезных действий: при вызове всегда выбрасывали UnsupportedOperationException. Поэтому в некоторых проектах появился код такого вида:
Thread worker = new Thread(() -> {}); try { worker.suspend(); } catch (UnsupportedOperationException e) { System.out.println("caught: " + e); }
Скомпилируем его с помощью JDK 21 и запустим на обеих версиях:
# JDK 21 caught: java.lang.UnsupportedOperationException # JDK 25, same class file Exception in thread "main" java.lang.NoSuchMethodError: 'void java.lang.Thread.suspend()' at Legacy.main(Legacy.java:5)
Метода больше не существует, поэтому JVM выбрасывает NoSuchMethodError. А это наследник Error, а не то исключение, к обработке которого был готов код.
Если перекомпилировать собственный код на JDK 25, проблема обнаружится сразу — компилятор сообщит cannot find symbol. Но повторная компиляция вашего приложения не поможет, если вызов находится в стороннем JAR‑файле, собранном много лет назад.
В разделе 8 я покажу, как находить такие зависимости.
4. Параметр java.locale.providers=COMPAT игнорируется — всего с одной записью в журнале
Начиная с JDK 9 данные локализации поступают из Unicode CLDR. Старые данные JRE оставались доступными через провайдер COMPAT, и многие команды включали его после того, как CLDR изменил форматы дат.
Особенно заметными изменения стали после выхода CLDR 42 в JDK 20: перед обозначениями AM/PM в англоязычных форматах времени появился узкий неразрывный пробел (U+202F). Из‑за этого перестал работать разбор строк, в которых использовался обычный пробел.
В JDK 23 данные COMPAT были удалены. Теперь соответствующий параметр не вызывает ошибку — система лишь выводит сообщение в журнал и игнорирует его.
Рассмотрим пример:
var fmt = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(Locale.US); String formatted = fmt.format(LocalDateTime.of(2026, 10, 8, 14, 30)); System.out.println("format -> " + formatted.replace(" ", "<U+202F>")); System.out.println("parse -> " + LocalDateTime.parse("Oct 8, 2026 2:30:00 PM", fmt));
Запустим его с параметром -Djava.locale.providers=COMPAT,CLDR:
# JDK 21 WARNING: COMPAT locale provider will be removed in a future release format -> Oct 8, 2026 2:30:00 PM parse -> 2026-10-08T14:30 # JDK 25 INFO: Invalid locale provider adapter "COMPAT" ignored. WARNING: COMPAT locale provider has been removed format -> Oct 8, 2026, 2:30:00<U+202F>PM parse -> DateTimeParseException: Text 'Oct 8, 2026 2:30:00 PM' could not be parsed at index 11
При одинаковом коде и одинаковом параметре JDK 25 добавляет в форматированную дату лишнюю запятую и невидимый символ перед PM. А попытка разобрать строку завершается ошибкой.
Аналогичные изменения затрагивают и числовые форматы. Например, для локали de‑CH в моём тесте COMPAT использовал в качестве разделителя групп разрядов символ ', а CLDR — ’ (U+2019).
Не стоит полагаться на локализованные форматы при работе с данными, которые впоследствии должны считываться программно. Используйте явно заданный шаблон:
DateTimeFormatter.ofPattern("MMM d, yyyy h:mm:ss a", Locale.US)
Либо применяйте ISO 8 601 для данных, которые сохраняются, передаются между системами или разбираются программой.
Если необходимы старые шаблоны, на странице JEP 252 появились дополнительные рекомендации по миграции.
Что касается непосредственно пробела перед PM, то в JDK 23 также была добавлена возможность более гибкого сопоставления пробелов при нестрогом разборе.
По умолчанию парсеры java.time работают в строгом режиме, поэтому нестрогий разбор нужно включать явно:
var lenient = new DateTimeFormatterBuilder() .parseLenient() .append(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)) .toFormatter(Locale.US); LocalDateTime.parse("Oct 8, 2026, 2:30:00 PM", lenient); // обычный пробел: 2026-10-08T14:30
При использовании строгого форматтера та же строка на JDK 25 вызывает ошибку на позиции 20.
Однако нестрогий разбор не возвращает прежний шаблон: запятая после года всё равно остаётся обязательной.
5. Новые предупреждения от sun.misc.Unsafe и JNI
Эти изменения пока ничего не ломают, но влияют на содержимое журналов и предупреждают о том, какие возможности перестанут работать в будущем.
Доступ к памяти через sun.misc.Unsafe. Методы работы с памятью этого API были объявлены устаревшими с перспективой удаления в JDK 23 (JEP 471). Начиная с JDK 24 первый вызов такого метода сопровождается предупреждением (JEP 498).
Вот что выводит небольшая программа, которая выделяет 8 байт памяти вне кучи, записывает туда значение типа long, а затем считывает его обратно:
# JDK 21 read back: 42 # JDK 25 WARNING: A terminally deprecated method in sun.misc.Unsafe has been called WARNING: sun.misc.Unsafe::allocateMemory has been called by OffHeap (file:/.../) WARNING: Please consider reporting this to the maintainers of class OffHeap WARNING: sun.misc.Unsafe::allocateMemory will be removed in a future release read back: 42
В реальных приложениях вызывающей стороной обычно оказывается библиотека — например, для сериализации, кэширования вне кучи или устаревшего сетевого взаимодействия. Предупреждение позволяет определить конкретную библиотеку.
В рамках JEP 498 предполагается, что в одном из будущих выпусков такие вызовы начнут выбрасывать исключения. Поэтому стоит проверить, не перешла ли более новая версия используемой библиотеки на VarHandle или MemorySegment из Foreign Function & Memory API (FFM API).
JNI. Начиная с JDK 24 (JEP 472), загрузка нативной библиотеки или привязка нативного метода без предварительного разрешения нативного доступа также сопровождается предупреждением:
WARNING: A restricted method in java.lang.System has been called WARNING: java.lang.System::loadLibrary has been called by Native in an unnamed module (file:/.../) WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module WARNING: Restricted methods will be blocked in a future release unless native access is enabled
Если приложение зависит от JNI — например, использует драйверы баз данных с нативными компонентами, библиотеки сжатия, криптографии или интеграции с операционной системой, — добавьте параметр --enable-native-access=ALL-UNNAMED либо явно укажите имена соответствующих модулей.
Другой вариант — добавить в манифест исполняемого JAR‑файла атрибут:
Enable-Native-Access: ALL-UNNAMED
Я рекомендую запускать тесты на JDK 25, задав обоим механизмам наиболее строгие значения параметров. Тогда CI обнаружит проблему уже сейчас, а не в тот день, когда изменится поведение JVM по умолчанию:
--sun-misc-unsafe-memory-access=deny --illegal-native-access=deny
С этими параметрами те же программы сразу завершаются исключениями UnsupportedOperationException: allocateMemory и IllegalCallerException: Illegal native access from an unnamed module.
6. Параметры запуска, которые теперь блокируют JVM или незаметно игнорируются
Параметры JVM годами накапливаются в скриптах запуска. Ниже перечислены те, поведение которых в JDK 25 изменилось по результатам моих тестов.
Параметр |
JDK 21 |
JDK 25 |
|
Предупреждение об устаревании |
Неизвестный параметр, JVM не запускается |
|
Принимается |
Неизвестный параметр, JVM не запускается |
|
ZGC без поддержки поколений |
Параметр игнорируется: поддержка удалена в версии 24.0 |
|
Принимается |
Предупреждение об устаревании |
|
Принимается |
Предупреждение об устаревании |
Параметры -t, -tm, -checksource, -cs и -noasyncgc были удалены в JDK 24 вместе с ‑Xfuture и теперь приводят к аналогичным ошибкам.
Если вы использовали -Xfuture, в примечаниях к выпуску предлагается заменить его на -Xverify:all.
Отдельного внимания заслуживает ZGC, поскольку здесь никаких ошибок не возникает.
В Java 21 параметр -XX:+UseZGC без дополнительных настроек включает прежний режим сборщика мусора без разделения объектов по поколениям.
В JDK 23 ZGC с поддержкой поколений стал режимом по умолчанию (JEP 474), а в JDK 24 однопоколенный режим был полностью удалён (JEP 490).
Это видно по журналу сборщика мусора:
# JDK 21, -XX:+UseZGC [gc,init] Initializing The Z Garbage Collector [gc,init] Using legacy single-generation mode # JDK 25, -XX:+UseZGC [gc,init] Initializing The Z Garbage Collector [gc ] Using The Z Garbage Collector
В JEP 439, в рамках которого был представлен ZGC с поддержкой поколений, утверждается, что такой сборщик должен стать «лучшим решением для большинства сценариев использования». Но его поведение отличается.
Если в Java 21 вы подбирали размеры кучи или тонко настраивали ZGC, после обновления обязательно перепроверьте потребление памяти и длительность пауз под нагрузкой.
7. Небольшие изменения с неприятными последствиями
В примечаниях к выпуску есть ещё несколько изменений, которые легко пропустить.
File.delete() в Windows больше не удаляет файлы, доступные только для чтения (JDK 25, JDK-8355954).
Теперь метод возвращает false вместо того, чтобы предварительно снять атрибут «только чтение»:
File f = Files.createTempFile("report", ".txt").toFile(); f.setReadOnly(); System.out.println("deleted: " + f.delete() + ", still exists: " + f.exists());
Результаты:
# JDK 21 on Windows deleted: true, still exists: false # JDK 25 on Windows deleted: false, still exists: true
Код очистки, который игнорирует возвращаемое значение delete(), а так устроена большая часть подобного кода, теперь будет оставлять файлы на диске.
Параметр -Djdk.io.File.allowDeleteReadOnlyFiles=true позволяет временно восстановить прежнее поведение, пока вы исправляете реализацию.
Можно самостоятельно снимать атрибут «только чтение» либо использовать Files.delete, который при неудаче выбрасывает исключение, а не просто возвращает false.
Настройки TLS по умолчанию стали строже.
В JDK 24 были отключены наборы шифров TLS_RSA_*, а в JDK 25 — использование SHA-1 для подписей в процессе TLS 1.2 handshake.
Теперь соединения со старыми внутренними серверами, специализированными устройствами или устаревшими системами партнёров, которые поддерживают только эти алгоритмы, могут завершаться исключением SSLHandshakeException.
Проверьте такие соединения до обновления. При необходимости алгоритмы можно повторно включить через java.security, но все связанные с этим риски безопасности вы берёте на себя.
8. Как обнаружить все эти проблемы до выхода в продакшен
Вот контрольный список проверок — в том порядке, в котором я сам их выполнял бы.
Изучите журнал сборки на JDK 21. Если ваш проект затронут проблемой с процессорами аннотаций из первого раздела, соответствующее предупреждение уже должно присутствовать в выводе. Дополнительно выполните компиляцию с параметром
-Xlint:deprecation,removalи проверьте предупреждения с пометкой[removal].Запустите jdeprscan
--for-removalиз текущей JDK 21 для приложения и JAR‑файлов его зависимостей. Утилита покажет все используемые API, объявленные устаревшими с перспективой удаления, включаяThread.suspend. В моём тесте jdeprscan из JDK 25 не обнаружилThread.suspendдаже с параметром--release 21, поэтому проверку следует провести именно инструментом JDK 21, прежде чем переходить на новую версию.
Вот результат проверки классов из этой статьи, скомпилированных на JDK 21 в каталог classes/:
$ jdeprscan --for-removal --class-path classes classes Directory classes: class Legacy uses deprecated method java/lang/Thread::suspend()V (forRemoval=true) class Sm uses deprecated class java/lang/SecurityManager (forRemoval=true) class Sm uses deprecated method java/lang/System::getSecurityManager()Ljava/lang/SecurityManager; (forRemoval=true) class Sm uses deprecated method java/lang/System::setSecurityManager(Ljava/lang/SecurityManager;)V (forRemoval=true) class Subj uses deprecated class java/security/AccessController (forRemoval=true) class Subj uses deprecated method javax/security/auth/Subject::getSubject(Ljava/security/AccessControlContext;)Ljavax/security/auth/Subject; (forRemoval=true)
В реальном проекте укажите в --class-path путь к зависимостям, а последним аргументом передайте JAR‑файл приложения или каталог с его классами.
Запустите
jdeps --jdk-internals, чтобы выяснить, какие компоненты используютsun.misc.Unsafeи другие внутренние API JDK.Проверьте конфигурации запуска — скрипты, Dockerfile, Helm‑чарты, переменные
JAVA_TOOL_OPTIONSиJDK_JAVA_OPTIONS— на наличие java.security.manager,java.locale.providers,-Xfuture,-XnoagentиZGenerational.Запустите полный набор тестов на JDK 25 с параметрами
--sun-misc-unsafe-memory-access=deny --illegal-native-access=deny. Затем изучите журнал запуска реального экземпляра приложения и обратите внимание на строкиWARNING:, которых раньше не было.Прочитайте разделы «Removed Features and Options» и «Other Notes» в примечаниях к выпускам JDK 22, 23, 24 и 25. Документы объёмные, но изменения, нарушающие обратную совместимость, составляют лишь небольшую их часть.
Если времени хватает только на три проверки, в первую очередь обратите внимание на процессоры аннотаций, параметр COMPAT и работу с файлами, доступными только для чтения.
Ни одна из этих трёх проблем не останавливает сборку или запуск приложения: процессоры просто перестают выполняться, COMPAT оставляет лишь одну запись в журнале при старте, а delete() возвращает false.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.