Java 24 уже тут. Огляд новинок і поради з міграції (Квітень 2025)

💡 Усі статті, обговорення, новини про Java — в одному місці. Приєднуйтесь до Java спільноти!

Всім привіт. Я Сергій Моренець, розробник, тренер, викладач, спікер і технічний письменник, хочу поділитися з вами своїм досвідом міграції проєктів з JDK 23 на JDK 24. Ця версія вийшла три тижні тому. І хоча це короткостроковий реліз з підтримкою протягом 6 місяців (наступний довгостроковий це JDK 25), проте там є цікаві зміни, заради яких варто познайомитися з цим оновленням.

Я вже писав про нові фічі в JDK 23, тому в цій статті пропущу ті, які не змінилися за півроку. Крім того, наведу результати benchmarks для деяких нових фіч. Сподіваюся, що ця стаття буде корисною для всіх, хто хоче більше дізнатися про новинки в Java 24, розібратися в тому, як провести міграцію застосунків на JDK 24, і які тут можуть бути складнощі.

Що нового в Java 24

18 березня вийшов черговий короткостроковий реліз Java — JDK 24, головна цінність якого в тому, що це останній такий реліз перед основною production-ready версією JDK 25. Таким чином, у Java розробників є час до вересня, щоб протестувати роботу всіх фітч, які знаходяться в режимі preview (ознайомлення), дати свій фідбек production використання.

Що ж увійшло в JDK 24? Перш за все це дві фітчі, які оголошені стабільними.:

  • Stream Gatherers
  • Class-File API

Про Stream Gatherers я вже детально писав раніше, і так як ця фіча увійшла в JDK 24 без змін, то тут особливо нема про що говорити. Class-File API дозволяє вам генерувати, парсити та трансформувати файли класів (байт-код). Історично так склалося, що цим займалися сторонні бібліотеки, такі як ASM, javassist чи ByteBuddy. Більш того, вплив того ж ASM виявився настільки значним, що його код довелося впровадити в сам JDK для виконання ряду функцій. Така техніка називається patching і використовується досить широко, наприклад, в Spring Framework теж є ASM код. Але тут є одна проблема. Після того, як JDK став виходити кожні 6 місяців, команда розробників ASM не встигає підтримувати нові версії Java. Виходить, що після виходу JDK 23, потрібен час на вихід нової версії ASM, потім оновлену версію доведеться додати в JDK 24 і тільки там можна використовувати фітчі з JDK 23. Більш того, проєкт ASM вийшов більше 20 років тому і досить застарів у технологічному плані, він містить багато старого коду, чий дизайн вже не відповідає.

Тому Class-File API дозволяє Java бібліотекам/фреймворкам відмовитися від сторонніх бібліотек та використовувати лише JDK для роботи з байт-кодом. Але тут не все так просто. Наприклад, Spring Framework 7, який вийде в листопаді цього року, як і раніше, використовуватиме мінімальну версію JDK 17, а це означає, що він не зможе використовувати Class-File API. Hibernate 7, який вийде цього року, також підтримуватиме JDK 17+. Тому пройде ще дуже багато часу, поки Java індустрія зможе на повну використовувати цю фітчу.

У JDK 24 досить багато фітч, які остаточно забороняють або навіть видаляють старий код і старі фітчі, визнані недоцільними, що не відповідають духу часу:

  • Видалення підтримки 32-бітної версії JDK для Windows.
  • Заборона використання 32-бітної версії JDK для Linux (буде остаточно видалена в JDK 25).
  • Заборона використання Security Manager.
  • Обмеження доступу до JNI.

Видалення підтримки 32-ї бітної версії JDK цілком очевидно, тому що ця її підтримка гальмує або заважає розвитку нових фітч Java (віртуальні потоки, foreign functions і т.д.). Що стосується Security Manager, то він спочатку був розроблений для захисту клієнтського коду (аплети) і, згідно з дослідженнями, мало використовується для безпеки серверного коду. Є деякі функції, які робили його корисним і в наші дні, наприклад, sandboxing — виконання недостовірного коду у спеціальному оточенні. Але зараз розробники JDK підтримують іншу точку зору, пропонуючи запускати такий код у контейнерах (Docker) або обмежуючи доступ за допомогою засобів ОС (cgroups). Ще одне популярне застосування Security Manager при десеріалізації також більше не є незамінним, оскільки ще в JDK 9 з’явилися десеріалізаційні фільтри, які дозволяють глобально контролювати десеріалізацію будь-якого Java об’єкта, у тому числі отриманого ззовні.

JNI — механізм для роботи з нативним кодом, який з’явився ще Java 1.1. У JDK 22 вийшла нова фіча Foreign Function and Memory, яка має замінити JNI. Але в будь-якому випадку використання нативного коду в JDK визнано потенційно небезпечним, тому зараз у консолі виводитимуться попередження при використанні JNI, а в майбутньому викидаються винятки (щоправда, їх можна буде відключити). Так що нативному коду нічого не загрожує, але дозвіл на його використання має бути явним і виходити від розробника.

Наступні дві інфраструктурні фітчі давно очікувані Java індустрією:

  • Стиснення заголовків об’єктів.
  • Синхронізація віртуальних потоків без прикріплення до платформних потоків (pinning).

Стиснення даних для економії пам’яті в купі вже не вперше зустрічається в JDK. Багато хто пам’ятає, як до JDK 11 додали таку фітчу як Compact Strings, яка дозволяла зменшити обсяг пам’яті, що займається рядком, якщо там зустрічаються лише ASCII символи. Але це торкнулося лише рядка.

Тепер нова фіча Compact Object Headers вплине на всі об’єкти в Java. Це досить революційна фіча, щоб поговорити про неї детальніше. Кожен об’єкт у JVM має заголовок, який складається з двох секцій: Mark Word і Class Word, кожна з яких спочатку займала по 8 байт. У Mark Word зберігається довідкова інформація про об’єкт, така як його hash code, статус у збирачі сміття та статус його блокування, якщо така є. У Class Word зберігається вказівник на інформацію про клас об’єкта.

Вже в Java 8 були зроблені перші кроки зі скорочення заголовка об’єкта за допомогою такої фітчі як Compressed Class Pointers. Тоді замість 64 біт вказівник на клас займав лише 32 біти. Це збіглося з іншою інфраструктурною зміною в Java 8, як заміна Permanent Generation на MetaSpace, де і зберігався тепер код класів. Таким чином, після Java 8 заголовки ваших Java-об’єктів були приблизно такою структурою:

Compact Object Headers пропонує подальший стиск цієї області до 64 біт:

Це досягається насамперед за рахунок того, що вказівник на клас стискається ще більше з 32 до 22 біт. Щоправда, тепер такі вказівники мають вирівнюватися по 210 = 1024 байт, що призведе до деякої фрагментації Metaspace для невеликих класів. І тепер ви можете використовувати максимум 4 мільйони класів у вашій JVM, щоправда, такі величезні проєкти в Java-світі ще не зустрічалися, а широке використання мікросервісів ще більше знижує цю ймовірність.

Що дає розробникам нова фіча? Здавалося б, економія 8 байт — не так багато. Але як показало дослідження, в середньому об’єкти в проєктах Java займають від 32 до 64 байт. Таким чином, ця фіча дозволяє заощадити 10-20% пам’яті в купі, що не так вже й мало.

Зрозуміло, нічого у світі не дістається безплатно. Така радикальна зміна структури заголовка торкнеться всіх підсистем JVM і вимагає ретельного тестування. Compact Object Headers не є preview, але увійшло JDK 24 як експериментальна фіча, тобто за замовчуванням вона відключена. Тільки benchmarks можуть показати її реальний вплив на продуктивність JVM, а реальні проєкти перейдуть на неї лише після повної впевненості у відсутності багів та інших прихованих проблем.

Синхронізація віртуальних потоків без прикріплення до платформних потоків — це фіча, на яку довго чекали як розробники, так і автори бібліотек. Віртуальні потоки увійшли до JDK 21 з проєкту Loom і дозволили писати реактивний код у звичайному синхронному стилі. Але в той же час ця фіча мала свої обмеження, наприклад, при використанні synchronized блоків віртуальний потік прикріплювався до платформних потоків і останній більше не міг перевикористовуватися. Тому авторам Java бібліотек доводилося перевіряти, чи є їх проєкт Loom-friendly, і постійно переписувати synchronized на Locks, як наприклад, робили автори Spring Framework тут і тут. Більше цього робити не потрібно, якщо ви використовуєте JDK 24, тому що тут поміняли логіку роботи із synchronized блоками.

Далі слідує велика група фітч, які знаходяться в режимі тестування і, зважаючи на все, будуть визнані стабільними в JDK 25:

  • Scoped Values.
  • Structured Concurrency.
  • Примітивні типи в патернах, instanceof і switch.
  • Vector API.
  • Гнучкі тіла конструкторів.
  • Декларації імпорту модулів.
  • Прості вихідні файли та instance main методи.

Всі ці фітчі (крім останньої) увійшли в JDK 24 без змін, та й в останній зміни мінімальні, тому особливо говорити про них нічого.

Продуктивність

Будь-яке дослідження нової технології має включати аналіз її ефективності, тим більше, що ми маємо такий чудовий інструмент як JMH. Для тестування нової фічі Compact Object Headers було обрано наступну конфігурацію:

  • JMH 1.37
  • JDK 24
  • Intel Core i9, 8 cores
  • 32 GB
  • 10 ітерацій обчислення, 10 ітерацій прогріву (warm-up)

Як типові операції були обрані:

  1. Integer boxing (оскільки ця операція кешує перші 256 значень, було обрано число, яке не потрапляло в кеш для створення нового об’єкта Integer)
  2. Обгортка в Optional
  3. Створення об’єкта для Java-запису
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 10)
@Measurement(iterations = 10)
public class Java24Benchmark {
     @Benchmark
     public Integer boxIntegerValue() {
          return 1000;
     }
     @Benchmark
     public Optional<String> optionalValue() {
          return Optional.of("abc");
     }
     @Benchmark
     public Contact recordValue() {
     return new Contact("123«, «test»);
     }
     static record Contact(String phone, String notes) {}

Результати вимірювання:

Benchmark Mode Cnt Score Error Units
Java24Benchmark.boxIntegerValue avgt 5 1.721 ± 0.032 ns/op
Java24Benchmark.optionalValue avgt 5 1.698 ± 0.013 ns/op
Java24Benchmark.recordValue avgt 5 2.067 ± 0.019 ns/op

Тепер передамо в JVM дві опції, які дозволять використання експериментальних фіч та містять фічу Compact Object Headers:

  • -XX:+UnlockExperimentalVMOptions
  • -XX:+UseCompactObjectHeaders

Результати вимірювання:

Benchmark Mode Cnt Score Error Units
Java24Benchmark.boxIntegerValue avgt 5 1.445 ± 0.011 ns/op
Java24Benchmark.optionalValue avgt 5 1.644 ± 0.016 ns/op
Java24Benchmark.recordValue avgt 5 1.661 ± 0.004 ns/op

У результаті можна констатувати, що ніяких помилок при використанні нової фічі не зафіксовано, а крім виграшу в зменшенні пам’яті, що використовується, ми також отримали прискорення по всіх трьох операціях:

  1. Boxing — 16%
  2. Optional — 3%
  3. Java запис — 20%

Переходимо на JDK 24

Спробуємо перейти на JDK 24 та оцінити його можливості. Перша спроба міграції проводилася через чотири дні після релізу. Її мета — перевірити, наскільки Java-індустрія готова до виходу нової версії Java.

Почнемо з того, що переведемо всі Dockerfile на JDK 24. І тут виявляється, що це не так просто для проєктів Gradle. Справа в тому, що поточна версія Gradle 8.13 не підтримує JDK 24, а нова версія 8.14 вийде лише у квітні. Чому знову затримка із підтримкою нових версій Java? Справа в тому, що така підтримка забезпечується бібліотекою ASM, яка йде разом з Groovy, а нова версія була випущена лише наприкінці лютого. Загалом знову бачимо, як ASM стає каменем спотикання при використанні нових релізів Java. Таким чином, упакувати наш проєкт у Docker image поки що не вийде.

Правда, зараз ми не можемо запускати збірку на Gradle 8.13, якщо у нас JDK 24, але ми можемо запустити Gradle на JDK 21, а відкомпілювати проєкт на JDK 24. Однак, і це неможливо, оскільки досі немає Docker images з для JDK 24 від Eclipse Temurin.

Але ми можемо зібрати проєкт та запустити тести локально. Запускаємо збірку і відразу отримуємо помилку:

Fatal error compiling: java.lang.ExceptionInInitializerError: com.sun.tools.javac.code.TypeTag :: UNKNOWN -> [Help 1]

Ця помилка добре відома і пов’язана з тим, що Lombok все ще не підтримує JDK 24 і навіть немає edge-версії для тестування. На жаль, вихід нової версії Java знову виявився повним сюрпризом для багатьох розробників бібліотек Java і тих, хто випускає Docker images.

Друга спроба міграції відбулася через 3 тижні після релізу JDK 24, коли вийшли:

  • Нові Docker images для Gradle, які дозволили збирати Gradle проєкти під JDK 24 (але не запускати Gradle під JDK 24).
  • Нова версія Lombok 1.18.38, яка підтримувала JDK 24.

Тому почнемо з того, що оновимо версію Lombok з:

lombokVersion=1.18.36

На:

lombokVersion=1.18.38

Потім оновимо версію Java:

java {
    sourceCompatibility = org.gradle.api.JavaVersion.VERSION_24
    targetCompatibility = org.gradle.api.JavaVersion.VERSION_24
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(24))
    }
}

Спробуймо спочатку запустити локальну збірку. І відразу отримуємо помилку від Error Prone:

error: An unhandled exception was thrown by the Error Prone static analysis plugin.
 Please report this at github.com/...​le/error-prone/issues/new and include the following:
 error-prone version: 2.30.0
 BugPattern: (see stack trace)
 Stack Trace:
 java.lang.NoSuchFieldError: Class com.sun.tools.javac.code.TypeTag does not have member field ’com.sun.tools.javac.code.TypeTag UNKNOWN’
 at com.google.errorprone.util.ASTHelpers.<clinit>(ASTHelpers.java:1301)

Ця помилка добре відома і пов’язана з тим, що JDK 24 видалили з внутрішнього класу одну з констант, в результаті всі статичні аналізатори коду, які її використовують, повинні оновитися і поміняти логіку своєї роботи. Нам потрібно перейти з Error Prone 2.30 до 2.37:

errorprone("com.google.errorprone:error_prone_core:2.37.0″)

Продовжуємо збірку та отримуємо помилку при запуску тестів:

Caused by: java.lang.IllegalStateException: Internal problem occurred, please report it. Mockito is unable to load the default implementation of class that is a part of Mockito distribution. Failed to load interface org.mockito.plugins.MockMaker
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.create(DefaultMockitoPlugins.java:105)
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.getDefaultPlugin(DefaultMockitoPlugins.java:79)
at org.mockito.internal.configuration.plugins.PluginLoader.loadPlugin(PluginLoader.java:75)
at org.mockito.internal.configuration.plugins.PluginLoader.loadPlugin(PluginLoader.java:49)
at org.mockito.internal.configuration.plugins.PluginRegistry.<init>(PluginRegistry.java:29)
at org.mockito.internal.configuration.plugins.Plugins.<clinit>(Plugins.java:26)
at org.mockito.internal.MockitoCore.<clinit>(MockitoCore.java:71)
at org.mockito.Mockito.<clinit>(Mockito.java:1741)
at org.mockito.junit.jupiter.MockitoExtension.beforeEach(MockitoExtension.java:156)
... 2 more
Caused by: java.lang.reflect.InvocationTargetException
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:499)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:483)
at org.mockito.internal.configuration.plugins.DefaultMockitoPlugins.create(DefaultMockitoPlugins.java:103)
... 10 more
Caused by: org.mockito.exceptions.base.MockitoInitializationException:
Could not initialize inline Byte Buddy mock maker.
It appears as if your JDK does not supply a working agent attachment mechanism.

Причина цієї помилки в тому, що ще в JDK 21 почалася боротьба з динамічним завантаженням Java-агентів. Поки що воно дозволено, але його можуть заборонити будь-якої миті і ситуацію може врятувати тільки чарівна опція -XX:+EnableDynamicAgentLoading. З якоїсь причини ця проблема нас торкнулася тільки зараз, і Mockito чомусь не зміг завантажити ByteBuddy-агент, але нас знову рятує перехід з Mockito 5.14.2 на останню версію 5.17.0. Така сама проблема переслідує й інші бібліотеки мокування, наприклад mockk

Тепер спробуємо зібрати та запустити наш застосунок. Поміняємо в Docker-скриптах версію Gradle/JDK з:

FROM gradle:8-jdk-21-and-23-alpine as gradle

На:

FROM gradle:8-jdk-21-and-24-alpine as gradle

Далі поміняємо версію JRE для запуску сервісів з:

FROM eclipse-temurin:23-jre-alpine

На:

FROM eclipse-temurin:24-jre-alpine

Збираємо Docker images, запускаємо контейнери, всі сервіси працюють без помилок.

Висновки

Загалом JDK 24 виглядає як підготовчий реліз до JDK 25, на який ми чекатимемо у вересні. У ній всього дві фічі, які визнані стабільними (Stream Gathers і Class-File API), які або використовуються лише Java-бібліотеками, або не настільки критичні для розробників. Міграція на нову версію пройшла без будь-яких серйозних ускладнень, але потрібно було зачекати щонайменше 3 тижні, поки вендори випустять нові версії своїх продуктів з підтримкою JDK 24..

З іншого боку, розробники JDK все більше обмежують доступ до тієї функціональності в Java, яка визнана внутрішньою і для якої вже готовий новий публічний API. Але досі ще жодна технологія не оголосила про те, що вона підтримує JDK 21+, не говорячи вже про JDK 25+, що говорить про те, що новий API буде використаний дуже нескоро.

👍ПодобаєтьсяСподобалось14
До обраногоВ обраному4
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі