JDK 16. Невеличкий крок для Java

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

При написанні четвертої частини свого циклу «Розробка Java додатків» я, як і в попередніх трьох частинах, намагаюся розповідати і використовувати останні (актуальні) версії технологій, які потраплять в production якщо не сьогодні, то вже завтра. Це стосується і Java, тому в недавніх главах я докладно розповідав про JDK 16 і її особливості, які виділив в цю статтю. Буду вдячний читачам за їх коментарі про те, наскільки успішний їх досвід роботи з останніми версіями Java.

Зараз вже складно повірити, що колись JDK випускалися з інтервалом в 3-4 роки, а JDK 7 і попередню 6-ю версію відокремлює взагалі 5 років. Правда, то були «темні часи», коли Java не дуже спішно переходило з влади Sun Microsystems під крило Oracle. JDK 9 була останньою версією, яка дотримувалася цієї схеми. Саме проблеми з 9-ї версією, яка неодноразово затримувалася з виходом через недостатнє тестування, привели Java-спiльноту до думки, що потрібен більш частий цикл релізів. В іншому випадку відтік розробників від Java до інших платформ міг би стати ще більш нищівним.

Новий підхід припускав випуск нових версій щопівроку. Це давало можливість розробникам придивитися до нових, часто експериментальних фіч і дати свій фідбек. Тому багато змін поставлялися в режимі «preview». І тільки після кількох ітерацій і доробок переходили в розряд стабільних і рекомендованих до використання.

З’явився поділ JDK на короткострокові (short-team) і довгострокові (long-term або LTS). Короткострокові версії підтримувалися максимум півроку і потім зникали з офіційних сайтів Oracle. Першим довгостроковим релізом став JDK 11, який буде підтримуватися мінімум до жовтня 2024 року, тому можна без будь-яких побоювань використовувати в production. Тим більше, що для JDK 8, який багато хто використовує до цих пір, комерційні поновлення припинилися в січні 2019 року. Правда, для навчальних і ознайомлювальних цілей поновлення виходитимуть до 2026 року. А наступним LTS релізом стане вересневий JDK 17.

Але повернемося до JDK 16. Наскільки цікаво нам, розробникам, використовувати цю версію? Всі зміни тут, як і раніше, можна розділити на дві умовні групи:

  • інфраструктурні зміни.
  • Зміни в API і синтаксисі мови.

Почнемо з першої групи, яка включає найбільші зміни:

1) Перехід на GitHub з Mercurial.

2) Портирування на Alpine Linux.

3) Elastic Metaspace.

4) Новий API для векторних обчислень — Vector API.

5) Зміни в збирача сміття ZGC.

Перехід на GitHub — це очікувана подія, і тепер весь JDK-код знаходиться в цьому репозиторії. Тут же знаходяться три сховища з проектами, які все ще в стадії активної розробки:

  • Loom — легковагі аналоги потоків, названі fibers.
  • Amber — зміни в синтаксисі мови.
  • Valhalla — value types, примітивні типи в generics, покращений volatile.

Все це досить важкі, але давно очікувані зміни в Java, які повинні спростити життя розробників і поліпшити ефективність Java-додатків. Дата їх релізу все ще невідома, наприклад, Valhalla розробляється з 2012 року, але все ще не досягла своєї зрілості. І навіть в JDK 17 їх очікувати не варто, деякі з Java експертів припускають, що вони увійдуть тільки в наступний LTS реліз (JDK 22 або 23).

Проект Amber тут стоїть окремо, бо він теж ще не закінчений, але він не є монолітним, а складається з безлічі невеликих змін, які легше перенести в нові релізи JDK. Деякі з них вже доступні в JDK 16. Але про це трохи пізніше, а поки повернемося до інфраструктурних змін.

Ще одна довгоочікувана фітча — перехід на Alpine Linux. Alpine — легка ОС, чий Docker-образ займає всього 4 Мб. Вона дуже зручна як базовий образ для тих Java-контейнерів, які ми використовуємо в production. Єдина заковика — до сих пір JDK використовувала бібліотеку glibc для комунікації з Linux, тоді як в Alpine Linux використовується бібліотека musl. Відповідно до JDK 16 ви могли використовувати Alpine для ваших Java-контейнерів, але довелося окремо встановлювати пакети glibc (а це ще додаткові 30 Мб). І ось в JDK 16 ви позбавлені від цієї необхідності.

Зміни в ZGC цілком очікувані. Починаючи з JDK 9 ми за замовчуванням використовуємо збирач сміття G1 (Garbage First). Але вже в JDK 11 і 12 з’явилися нові експериментальні збирачі сміття — Z і Shenandoah відповідно. У 15-й версії вони були оголошені стабільними і все ще продовжують еволюціонувати. Shenandoah пропонує покращений алгоритм роботи, в порівнянні з G1, споживаючи більше пам’яті, але з меншими паузами. ZGC — абсолютно новий збирач сміття, розрахований на дуже великі розміри купи (аж до терабайтов), але при цьому з максимальним часом паузи 10 мілісекунд. Зараз команда Oracle-розробників продовжує збирати фідбек про їх використання, так що можливо в майбутньому хтось із них замінить G1.

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

  • Pattern matching для instanceof.
  • Записи (records).

І одна фіча, яка все ще в режимі preview — sealed classes (запечатані класи). Sealed classes дозволяють більш точно вказувати правила для інкапсуляції ваших класів/інтерфейсів. Вони більше застосовні для самої JDK і Java бібліотек, ніж для прикладних проектів. Для того, щоб зрозуміти їх призначення, розглянемо класи відомі Integer і Number. Клас Integer оголошений як final:

public final class Integer extends Number
implements Comparable<Integer>, Constable, ConstantDesc {

Це зроблено для того, щоб розробники використовували принцип composition over inheritance, використовуючи тільки публічний API класів, а не наслідуючи їх, заважаючи їх рефакторингу і розвитку.

У класу Integer базовий клас — Number, і його ніяк не можна оголосити final, тому будь-який розробник може його успадкувати. А ось в JDK 16 тепер можна написати так:

final class Integer extends Number {
}
sealed class Number permits Integer {
}

І тепер будь-хто, хто наважиться вибрати Number, як базовий клас, пошкодує про це, тому що його код перестане компілюватися:

class BigInteger extends Number {
}

Аналогічно і з інтерфейсами. В Java до цього моменту будь-який клас міг реалізувати публічний інтерфейс (якщо ви не використали Java модулі). Тепер же ви також можете обмежити список «щасливчиків»:

sealed interface Shape permits Square {}
final class Square implements Shape {}

При цьому клас повинен бути явно оголошений як final, sealed або non-sealed.

Pattern matching для instanceof — фiча, яка дозволяє писати більш короткий instanceof, наприклад замість:

if (value instanceof AbstractEntity) {
AbstractEntity ref = (AbstractEntity) value;
}

Тепер можна написати:

if (value instanceof AbstractEntity ref) {
int id = ref.getId();

Такий підхід більш зручний, але не є межею досконалості. Є мови, де більш інтелектуальний компілятор дозволить взагалі обійтися від нової змінної:

if (value instanceof AbstractEntity) {
int id = value.getId();

Будемо сподіватися, що ця фітча буде розвиватися і вдосконалюватися з часом.

Записи дозволяють створювати immutable типи даних без додаткових бібліотек, таких як Lombok. Наприклад, замість:

@Data
private static class InputDTO {
private String id;
}

Тепер можна написати:

private static record InputDTO(String id) {
}

Java компілятор згенерує read accessor методи, конструктор з усіма параметрами і перевизначити toString/equals/hashCode. Тому використовувати записи більш безпечно, тому що в звичайних класах ви можете забути перевизначити ці методи. З іншого боку, це наштовхує на думку, що найбільш зручно використовувати записи в колекціях, де як раз використовуються equals/hashCode.

Єдине, з чим можна обпектися — в записах використовуються не геттери, а read accessor методи, які не дотримуються конвенцію JavaBeans:

InputDTO dto = new InputDTO("1");
String id = dto.id();

Тому, якщо ви використовуєте записи там, де йде звернення до полів через Reflection API (DI фреймворки), то вони повинні явно підтримувати новий формат.

Це не всі зміни, які є в проекті Amber, в наступних версіях з’явиться pattern matching для instanceof, масивів і записів.

В цілому, як ви бачите, список змін в JDK 16 досить значний. Будемо чекати великих фіч, таких як Loom/Valhalla, які зроблять Java більш сучасною і привабливою мовою.

👍НравитсяПонравилось13
В избранноеВ избранном5
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

На днях пробовал поднять на ней проект на spring boot изначально написанном на 11-й Яве. В итоге половина плагинов в Gradle отказалась работать (maven пользовать не могу, команда начала до меня), в том числе базовые типа application. В общем очередной релиз который поломал обратную совместимость, я не понимаю что там в голове у Oracle. Они привносят bracking changes с каждым релизом, половина проектов из-за этого все ещё 1.8 и даже не собираются абгрейдиться. Новые версии уже задумываются на Node.js, Go, Python и т.п. лишь бы с Oracle не связываться.

Просто 16 — це не LTS. Тому автори плагінів не поспішають, та і швидше за все їм за це ніхто не платить.

Новые версии уже задумываются на Node.js, Go, Python и т.п. лишь бы с Oracle не связываться.

А це ви дуже даремно. На підході проекти з валхали та ліліпута. ПІсля них джава це буде просто ракета.

А можна детальніше шо за проекти? Людям не з джава-світу не зовсім зрозуміло

Це дуже великі проекти. З основних змін я виділив би наступні:

— Зменшення загаловку об’єкта. Зараз хідер любого обєкта в джава це ~8 байт (залежить від JVM і налаштувань). Тобто, new Integer(1000) займає 16 байт (8 хідер + 4 інт + 4 вирівнювання). Мета зменшити розмір хідеру в 2 рази. Тобто обєкт Integer почне займати 8 байт (4 хідер + 4 інт), а не 16 як зараз. Якщо цю фічу зроблять, то кожен з існуючих джава серверів почне жерти менше памяті. По моїм підрахункам десь на ~10% в середньому. Але це, звісно, сильно залежить від проекту.
— Аналог структрур (struct), або inline class. Тобто можливість створювати структури, які будуть розміщуватись не в хіпі, а на стеку. Для великої кількості алгоритмів, де важлива локальність данних це стане бімбою. Легко можна буде отримати 2-10х приріст за рахунок цього. В джаві вже давно є така оптимізація — scalar replacement. Але вона рідко коли спрацьовує. Тепер же, по суті, її можна буде використати саме там, де треба, а не чекати кота шрєдінгера.
— Наближення примітивних враперів (Integer) до примітивів (int). Зараз, щоб працювати з джава колекціями потрібно використовувати врапери примітивів. Не можна створити List int. І це величезна проблема як для швидкодії, так і для памяті, що викоритовується. Йде робота, над так званими value objects. Мета яких — запропонувати аналог List int.

А пройде ще років 70, може дійде й до ліквідації функцій керування потоками в Object з перенесенням в більш спеціалізовані класи.

Така ціна зворотньої сумістності. На жаль, тут нічого не зробиш.

Возможно, лет через 70 в java появятся extension methods, и всю deprecated хрень можно будет вынести в отдельные compatibility библиотеки.

Или, лет через 100 в jdk добавят урезаный ломбок, чтобы при надобности, генерировать deprecated хрень при компиляции.

Это всё не тормозит. А вот конкурентность Object и соответственно механизм доступа к любому объекту через функцию, а то и через сообщения — это реальная жопа в производительности, делающая жабу в разы уступающей тому же С++. Хотя ровно никаких причин с этим мириться нет.

Чому не зробиш? Додати до типу якийсь функціонал не проблема. Тобто, для сумісності зробити параметр запуску JVM. Бо дууже мало хто використовує Object для керування потоками. Для цього вже давно створений набір java.util.concurrent

Во-первых, градл — говно.
Во-вторых, учитывая что брейкинг ченжей не анонсировало, а также п.1., я уверен, что в вашем случае проблема как раз в граде, а не в ждк.
Насчёт половины проектов на восьмерке хз, помоему на 11 только ленивый не перешёл

искусствоведов группа тихо
восторженно глядит на холст
и вдруг один седой и строгий
отчётливо сказал говно

Во-первых, градл — говно.

прям внещапно. А что тогда?

А мавен чем не устраивает? )

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

Чистил недавно ноут от старых проектов, у одного кастомера было много реп. Всё на блядском градле. Захожу в папку с репами — общий вес 2.2 гб. Прошёлся по всем папкам ради интересу удалил папки градла. О чудо, осталось 180 мб. Что туда насрал градл — неизвестно, но такое поведение это дичь.

А мавен чем не устраивает? )

Если вас устраивает мавен, то градл вам не нужен.

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

так, да не так.
по факту градл — дефолтный сборщик, если лид или архитектор хипстота, и пофиг даже если проект уровня хеловорлда.

Не зовсім зрозумів, як поєднується зворотна сумісність в JDK і сумісність плагінів в Gradle.
Якщо у вас поламалися плагіни в Gradle, швидше за все після переходу на Gradle 7, то це проблема або Gradle, або плагінів, але ніяк не Oracle.
Ми свої два проекти перевели на JDK 16/Gradle 7 без проблем.

Шо там с перформансом нового гербедж коллектора ?

It depends, як завжди. Не спробуєш — не взнаєш.

Есть опыт? Можно развернутее? Очень интересно, если есть реальные кейсы.

да. прод сервисы, 20-40к rps на хост, p50 < 1ms, ZGC пашет что трактор, только что специально с одного из прод инстансов взял логи. Памяти 2GB
Throughput : 99.774%
Avg Pause GC Time 2.04 ms
Avg creation rate 223.95 mb/sec

Еще один сервис, памяти уже 98GB, кол-во запросов на хост не помню, но число тоже достаточно большое:
Throughput : 99.841%
Avg Pause GC Time 13.1 ms
Max Pause GC Time 27.6 ms
Avg creation rate 3.47 gb/sec
Этот же сервис раньше работал на G1 (при этом этот G1 был затюнен по самое), нашел старые логи, и вот там было
Throughput : 98.308%
Avg Pause GC Time 72.1 ms
Max Pause GC Time 1 sec 900 ms (!!!)

Уаще бімба-ракета
Можно уточню? А какой сценарий приложений? Я имею в виду, есть ли какие-то там ин-мемори стрелки, тяжёлые кеши?
Так то я видел сборки мусора и по 70 секунд )

первое — по сути прокси над мемкешем и редисом, добавляющее кластерную логику, компрессию, логику специфическую для компании и тп. То есть потоки данных достаточно простые — API (gRPC) -> Proxy -> Memcached/Redis -> Proxy -> Api (gRPC)
второе — по сути persistent layer, скрывающий базы данных от пользователей. То есть пользователи ходят не в базу а в этот сервис. Он же обеспечивает кеширование, секьюрность, кластеры, опять же специфическую для компании логику для баз данных, и тп и тд.

Абсолютно не раскрыта тема records.

Основная цель этого улучшения — это не удовлетворить хипстоту с их ломбоками, а убрать излишнюю работу JVM c «псевдо-объектами», как например, дтошки. Главна работа там проведена не на уровне синтаксиса, сахара и вот этого всего геттеры-вс-аксессоры-вс-поле, а на уровне того, что виртуальная машина работает с рекордами не так, как с обычными объектами. Потребление хипа в приложении, построенном на рекордах (а 99% спама внутри JVM это как раз «рекорды»), емнип, на порядок меньше.

Прежде всего, имхо, это должно заафектать не хипстоту за идейками, объявляющих record MyDto, а ин-мемори стореджи, кеши, и прочие ин-мемори компьютейшн, которые проведут адопшн рекордов в следующих версиях, что понизит потребление памяти а значит и цену JVM-based продакшна.

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

які не дотримуються конвенцію JavaBeans

Ну и слава б-гу.
Конвенция JavaBeans в части геттеров просто ужасна.

ZGC — абсолютно новий збирач сміття, розрахований на дуже великі розміри купи (аж до терабайтов)

Не совсем верно.
Он рассчитан на любые хипы, точнее от 8 МБ до 16 ТБ.
Кроме того,

By default, ZGC uncommits unused memory, returning it to the operating system
сновная цель этого улучшения — это не удовлетворить хипстоту с их ломбоками, а убрать излишнюю работу JVM c «псевдо-объектами», как например, дтошки. Главна работа там проведена не на уровне синтаксиса, сахара и вот этого всего геттеры-вс-аксессоры-вс-поле, а на уровне того, что виртуальная машина работает с рекордами не так, как с обычными объектами.

Ніт. Це inline class, який ми ще не скоро побачимо. Швидше за все вже в 21-й версії. Але ніяк не рекорди. Рекорди якраз це чисто гетери і сетери, щоб позбавитись від любителів ломбока.

Ніт

Хм, возможно я ошибся, но мне кажется, читал я это именно про рекорды. Инлайн классы вроде еще не в превью, емнип.

Рекорди якраз це чисто гетери і сетери

они immutable

Записи (records) з’явилися ще в JDK 14, а стаття більше про JDK 16. А по-друге, це дуже велика тема і заслуговує на окрему статтю.

if (value instanceof AbstractEntity ref) {
int id = ref.getId();

Чи можу я сподіватися, що ref == value всередині конструкції? Тобто, зможу таке:
if (value instanceof IFace1 ref) if (ref instanceof IFace2 ref2){
}
Чи як кошерно таку конструкцію тепер записувати? Чи краще взагалі не використовувати синтаксичний цукор, та писати як звикли раніше?

А от нащо Жабі генерить read accessor методи — зрозуміло не дуже. Нащо потрібен

String id = dto.id();

замість String id = dto.id;
Якщо код генерується самою жабою, то що завадило створити окремий модифікатор доступу замість виклику методу? Пояснюю, в чому проблема: виклик методу — це вельми повільна операція порівняно із прямим доступом. І коли таких викликів мільйарди, воно реально тупитиме. Навіщо створювати гетер та щоразу його викликати замість тупого контролю доступу на етапі компіляції? Те саме із записом. Таке відчуття, ніби за десятки років еволюції так складно зрозуміти, що те що робиться за межами коду (тобто автоматично), можна оптимізувати як завгодно. Тим більше що жодного реального захисту JVM не робить, якщо є бажання, ніщо не заважає записати дані напряму, тобто це лише спосіб не вистрілити собі в ногу.

if (value instanceof IFace1 ref) if (ref instanceof IFace2 ref2){
}

Ну вообще какбы ожидаемо что ref не будет доступна вне блока { ... } перед которым стоял var instaceof %Class% ref
И я уверен, что так и будет, хотя сам не пробовал еще.

Ні, ref — це просто нове оголошення змінної і звичайно, у неї область видимість — це block if. Ви не зможете до неї звернутися поза блоком.

Те що блок if не має {} це все одно блок. Лише синтаксичний цукор.
Але питання було: Чи можу я сподіватися, що ref == value всередині конструкції?

Тобто, як саме відбуватиметься динамічна типізація. Бо value може мати окрім властивостей того класу що в аргументі instanceof що й реалізацію якихось інтерфейсів. Чи матиме ref реалізацію тих самих інтерфейсів. І оскільки це кастинг того самого об′єкта, що буде якщо скажімо присвоїти цей ref всередині блока якійсь зовнішній для блока змінній? Варіанти: буде створено замкнення; буде втрачено властивості, надані через instanceof конструкцію; буде створена нова змінна як при звичайному перетворенні типів (хоча ти ж розумієш, що під капотом JVM нема нічого «звичайного», там декілька механізмів).

Те саме питання — що станеться, коли ця змінна попаде під return в блоці?

Наскільки я розумію Java, то ref == value має відбуватися. Тобто тайпкастингу не відбуватиметься взагалі, через те що умова if не перетворює об′єкти, а лише перевіряє приналежність. Тобто, у блоці просто не відбувається перевірка компілятором, от і усе. Фактично цей синтаксичний цукор знімає warning і нічого більше.

А що до скорочення, можу я писати так?
if(value instanceof IFace1 ref && ref instanceof IFace2 ref2){...}
А так? Чи буде змінна мати властивості обох інтерфейсів? Бо такий запис читається краще.
if(value instanceof IFace1 && value instanceof IFace2 ref2){...}

По-моему, ты сейчас докапываешься до какойто херни, и вопросы слегка подвысосаны из пальца.

Чи матиме ref реалізацію тих самих інтерфейсів

== не проверяет реализацию каких-либо интерфейсов, да? )

Ref будет иметь тип, указанный в instanceof, вот тебе и проверка. Это, в принципе, отвечает на все твои вопросы.

value instanceof X ref
это просто шорткат для бойлерплейта
X ref = (X) value;
И это еще раз отвечает на все твои вопросы.

«Какая-то херня» называется читаемостью кода и лёгкостью поиска ошибок. Дьявол — в деталях.

Читаемость кода резко сокращает количество багов. В отличие от львиной доли синтаксического сахара, который сокращает количество кода ценой радикального снижения читабельности.

Вообще я с тобой согласен, но не в данном случае.
В данном случае речь идет об убирании совершенно лишнего бойлерплейта в фиксированной идиоме.

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

Если бы синтаксис писал я, то и ref бы счёл лишним. Зачем плодить новую сущность, если у меня есть переменная value, которой проверена принадлежность к классу или интерфейсу, так почему бы дальше не связать код без участия программиста? Вообще ж никаких проблем. Если кажется, что встрянет лишнее преобразование типов — так нифига, если программист написал проверку, значит этот кастинг ему нужен. Не был бы нужен, то и не проверял бы.

замість String id = dto.id;

Ну в Kotlin ви так і напишете. Тому що в Kotlin якраз і є properties, а в Java поля.

В тому і проблема, що це лише синтаксис. Але під ним можна повністю ліквідувати гетери як такі там де вони не потрібні. А це 99.999% усіх звернень до змінної на читання. Доля звернень, які потребують якогось особливого гетера просто мінімальна, і лише для них потрібна реалізація «під капотом». Усі інші отримали б зріст продуктивності на рівному місці.

С elastic metaspace уже кто-то пробовал экспериментировать? Потому как шкварка жирная, то чего реально не хватает Java последние лет 15.

inal class Integer extends Number

Спасибо, заменил на final

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