Хабрахабр

Пришло время Java 12! Обзор горячих JEP-ов

Это был долгий путь, и до конца добрались немногие. Прошло полгода, а значит — время устанавливать новую Java! Из интересных JEP-ов отвалились сырые строки, а вот об оставшемся мы поговорим под катом.

Точные даты определены на странице проекта. Выпуск новой версии Java проходит согласно новому "ускоренному" релизному циклу длиной примерно в полгода. Для JDK 12 существовало несколько основных фаз:

  • 2018/12/13 — Первая фаза замедления (в этот момент делается форк от основной ветки в репозитории);
  • 2019/01/17 — Вторая фаза замедления (завершить всё, что только можно);
  • 2019/02/07 — Релиз-кандидат (фиксятся только самые важные баги);
  • 2019/03/19 — Релиз, General Availability. < — вы находитесь здесь

Да в сущности, ничего — мы только что пришли к финишу, и барски взираем на любителей легаси с высоты новенькой свеженькой JDK 12. Что нам с этого расписания?

Интересней, не развалится ли всё к морским чертям. Когда выходит новая не-LTS версия, обычно всем глубоко наплевать на новые фичи.

Конечно, баги есть, много, но не в JDK 12 🙂 Судя по джире — всё в норме:

Процитирую запрос, чтобы вы в точности понимали, что такое "норма":

project = JDK AND issuetype = Bug AND status in (Open, "In Progress", New) AND priority in (P1) AND (fixVersion in (12) OR fixVersion is EMPTY AND affectedVersion in (12) AND affectedVersion not in regexVersion("11.*", "10.*", "9.*", "8.*", "7.*", "6.*")) AND (labels is EMPTY OR labels not in (jdk12-defer-request, noreg-demo, noreg-doc, noreg-self)) AND (component not in (docs, globalization, infrastructure) OR component = infrastructure AND subcomponent = build) AND reporter != "Shadow Bug" ORDER BY priority, component, subcomponent, assignee

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

Более формально общение с багами задекларировано в специальном документе, JEP 3: JDK Release Process, владельцем которого является наш бессмертный стюард по неспокойным волнам Java-океана — Марк Рейнхольд.

Надо поставить в багтрекере метку jdk$N-defer-request в которой N указывает, с какого именно релиза хочется перенести, и оставить комментарий, первая строка которого — Deferral Request. И в особенности стоит докопаться абзаца, рассказывающего, кто виноват и что делать, как переносить тикеты, если к 12 релизу уже не успеть. Дальше за ревью всех таких запросов берутся лиды соответствующих областей и проектов.

Метка jdk$N-defer-request label никуда не исчезает. Проблемы прохождения TCK нельзя проигнорировать подобным образом — гарантируется, что Java остаётся Java, а не чем-то жабообразным. Интересно, что они делают с людьми, которые нарушают правило неудаления метки — предлагаю скормить морским свинкам.

Попробуем такой запрос: Тем не менее, таким образом можно посмотреть, сколько багов перенесено на JDK 13.

project = JDK AND issuetype = Bug AND status in (Open, "In Progress", New) AND (labels in (jdk12-defer-request) AND labels not in (noreg-demo, noreg-doc, noreg-self)) AND (component not in (docs, globalization, infrastructure) OR component = infrastructure AND subcomponent = build) AND reporter != "Shadow Bug" ORDER BY priority, component, subcomponent, assignee

Негусто. Всего 1 штука, JDK-8216039: "TLS with BC and RSASSA-PSS breaks ECDHServerKeyExchange". Если этот довод всё ещё не помог, то, как ваш адвокат, рекомендую попробовать успокоительное.

Поэтому я на всякий случай делю фичи на внешние и внутренние. Ясно, что большинство фичей затрагивает не пользователей (Java-программистов), а разработчиков самого OpenJDK. Внутренние можно пропустить, но я обижусь, столько текста написал.

189: Shenandoah: A Low-Pause-Time Garbage Collector (Experimental)

Вкратце, люди не любят, когда Java тормозит, особенно если SLA требует отзывчивости порядка 10-500 миллисекунд. Внешняя фича. Компромисс таков, что мы обмениваем CPU и RAM на уменьшение задержек. Теперь у нас есть бесплатный низкопаузный GC, который пытается работать ближе к левому краю этого диапазона. Оставшиеся небольшие паузы связаны с тем, что всё равно надо искать и обновлять корни графа объектов. Фазы маркировки и уплотнения хипа работают параллельно с живыми тредами приложения.

Если ничего из сказанного не имеет для вас никакого смысла — не беда, Shenandoah просто работает, вне зависимости от понимания или непонимания глубинных процессов.

Если вы в целом понимаете, как работают GC но не представляете, чем там может заниматься разработчик — рекомендую взглянуть на хаброперевод чудесной Лёшиной статьи "Самодельный сборщик мусора для OpenJDK" или на серию JVM Anatomy Quarks. Работают над ним Алексей Шипилёв, Кристина Флад и Роман Кеннке — нужно сильно постараться, чтобы не знать об этих людях. Это очень интересно.

Учитывая, что Shenandoah — одна из важнейших фичей JDK 12, похоже, лично мне придётся на этом со сборками от Oracle распрощаться. Экстримально важно. Oracle решили не поставлять Sheandoah ни с какой из своих релизных сборок — ни с той, что на jdk.java.net, ни с той, которая на oracle.com. Сборку от RedHat, Azul и других всё ещё ждём, пост будет дополняться.

230: Microbenchmark Suite

Если вы хоть раз пытались писать микробенчмарки, то знаете, что это делается на JMH. Внутренняя фича. К сожалению, не всё, что делается в мире "нормальных" приложений можно применить внутри JDK. JMH — это фреймворк для создания, сборки, запуска и анализа микробенчмарков для Java и других JVM-языков, написанный сами понимаете кем (все совпадения случайны). Например, вряд ли мы когда-то увидим там нормальный код на Spring Framework.

Посмотреть можно в jdk/jdk/test/micro/org/openjdk/bench (можете в браузере прямо посмотреть, этот путь — ссылка). К счастью, начиная с 12 версии можно использовать хотя бы JMH, и уже есть набор тестов, которые на нём написаны.

Например, вот как выглядит тест на GC.

Напомню что здесь у нас не StackOverflow, и использовать код из копипасты, здесь и далее в статье, запрещено без чтения и соблюдения всех лицензий из соответствующего файла и проекта OpenJDK вообще, иначе на каком-нибудь суде у тебя с легкостью отсудят последние носки.

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class Alloc } //...
}

325: Switch Expressions (Preview)

Коренным способом изменит ваш подход к написанию бесконечных свичей длиной более двух экранов. Внешняя фича. Глядите:

Virgin Java Switch vs ...

int dayNum = -1;
switch (day) { case MONDAY: case FRIDAY: case SUNDAY: dayNum = 6; break; case TUESDAY: dayNum = 7; break; case THURSDAY: case SATURDAY: dayNum = 8; break; case WEDNESDAY: dayNum = 9; break;
}

Почему плохо: много букв, можно пропустить break (особенно, если ты наркоман, или болеешь СДВГ).

… vs Chad Java Swtich Expression!

int dayNum = switch (day) { case MONDAY -> 0; case TUESDAY -> 1; default -> { int k = day.toString().length(); int result = f(k); break result; }
};

Почему хорошо: мало букв, безопасно, удобно, новая клёвая фича.

Да, lany, да? Бонус: если ты садист, то тебе доставит глубочайшее удовлетворение, как тысячи разработчиков IDE теперь мучаются с поддержкой этой фичи. Его можно поймать после доклада 6-ого апреля и мягко попросить выдать все грязные подробности.

Это preview фича, просто так она не заработает! При компиляции, в javac нужно передать опции командной строки --enable-preview --release 12, а для запуска через java — один только флаг --enable-preview.

334: JVM Constants API

Разработчикам хочется манипулировать классфайлами. Внутренняя фича. По крайней мере, так сказал Брайан Гёц, который владеет этим JEP-ом 🙂 Всё это часть более масштабного поля брани, но пока не будем углубляться. Нужно делать это удобно, и это постановка задачи.

Порыться в этой свалке можно с помощью инструкции ldc — "load costant", поэтому всё это барахло называется loadable constants. В каждом Java-классе есть так называемый "константный пул", где находится свалка либо каких-то значений (вроде стрингов и интов), или рантаймовые сущности вроде классов и методов. Есть ещё специальный случай для invokedynamic, но неважно.

Первое желание — просто наделать соответствующих Java-типов, но как представить им "живой" класс, структуру CONSTANT_Class_info? Если мы работаем с классфайлами, то хотим удобно моделировать байткодные инструции, и следовательно — loadable constants. Начнём с того, что не все классы можно загрузить в VM, а описывать-то их всё равно надо! Class-объекты зависят от корректности и консистентности загрузки классов, а с загрузкой классов в Java творится адовая вакханалия.

Хотелось бы как-то попроще управлять вещами вроде классов, методов и менее известными зверьми вроде method handles и динамических констант, с учётом всех этих тонкостей.

1), каждая из которых описывает какую-то конкретный вид констант. Это решается введением новых value-based типов символических ссылок (в смысле JVMS 5. Они живут в пакетах вроде java.lang.invoke.constant и есть не просят, а на сам патч можно взглянуть здесь. Описывает чисто номинально, в отрыве от загрузки классов или вопросов доступа.

Уже в JDK 9 сложилась странная ситуация, когда Oracle и Red Hat одновременно поставили на боевое дежурство свои ARM-порты. 340: One AArch64 Port, Not Two
Внешняя фича. И вот мы видим конец истории: 64-битную часть оракловского порта убрали из апстрима.

В разработке этого JEP поучаствовала компания BellSoft, а её офис расположен в Питере, рядом с бывшим офисом компании Oracle. Можно было бы долго копаться в истории самому, но есть способ лучше.

Поэтому я сразу обратилился сразу к Алексею Войтылову, CTO компании BellSoft:

Начиная с JDK 9 для ARM мы сфокусировались на улучшении производительности порта AARCH64 для серверных применений и продолжили поддерживать 32-битную часть ARM порта для встраиваемых решений. "BellSoft выпускает Liberica JDK, которая, помимо x86 Linux/Windows/Mac и Solaris/SPARC, поддерживает и ARM. На настоящий момент он более производительный (см, например, JEP 315, который мы заинтегрировали в JDK 11) и, начиная с JDK 12, поддерживает все фичи, присутствовавшие в порте от Oracle (последнюю, Minimal VM, я заинтегрировал в сентябре). Таким образом на момент выхода JDK 11 сложилась ситуация, когда 64-битную часть порта от Oracle никто не поддерживал (включая Oracle), и OpenJDK сообщество приняло решение удалить ее, чтобы сфокусироваться на AARCH64 порте. В итоге OpenJDK сообщество получило один порт на AARCH64 и один порт ARM32, что, безусловно, облегчает их поддержку." Поэтому в JDK 12 я с удовольствием помог Bob Vandette удалить этот рудимент.

341: Default CDS Archives

Проблема в том, что при старте Java-приложения загружаются тысячи классов, отчего создаётся ощущение, что Java ощутимо тормозит при старте. Внутренняя фича. Чтобы исправить проблему издревле практикуются различные ритуалы. Да кому тут врать, это не просто "ощущение" — так оно и есть.

Она позволяет упаковать весь этот стартапный мусор в архив какого-то своего формата (вам не нужно знать — какого), после чего скорость запуска приложений возрастает. Class Data Sharing — это фича, пришедшая к нам из глубины веков, как коммерческая фича из JDK 8 Update 40. А через некоторое время появился JEP 310: Application Class-Data Sharing, который позволил обходиться таким же образом не только с системными классами, но и классами приложений.

Вначале мы дампим классы командой java -Xshare:dump, и потом запускаем приложение, сказав ему использовать этот кэш: java -Xshare:on -jar app.jar. Для классов JDK это выглядит так. Вот вы знали об этой фиче? Всё, стартап немного улучшился. Много кто не знает до сих пор!

Согласно документации, если дистрибутив Java 8 устанавливался с помощью инсталлятора, то прямо в момент установки он должен запускать нужные команды за тебя. Здесь выглядит странно вот что: зачем каждый раз самостоятельно ритуально писать -Xshare:dump, если дефолтный результат выполнения этой команды немножко предсказуем ещё на этапе создания дистрибутива JDK? Но зачем? Типа, инсталлятор тихонечко майнит в уголке. И что делать с дистрибутивом, который распространяется не в виде инсталлятора, а как зипник?

Даже для ночных билдов (при условии что они 64-битные и нативные, не для кросс-компиляции). Всё просто: начиная с JDK 12 архив CDS будет генериться создателями дистрибутива, сразу же после линковки.

Таким образом, просто сам факт обновления на JDK 12 ускоряет запуск приложения! Пользователям даже не нужно знать о наличии этой фичи, потому что, начиная с JDK 11, -Xshare:auto включена по-умолчанию, и такой архив подхватится автомагически.

344: Abortable Mixed Collections for G1

Честно говоря, я ничего не понимаю в работе G1 объяснение фичей GC дело неблагодарное, т.к. Внутренняя фича. Для большинства же людей, GC — это какой-то чёртик из табакерки, которому можно накрутить в случае чего. требует понимания деталей его работы и от объясняющего, и от понимающего. Поэтому проблему надо объяснить как-то попроще.

Проблема: G1 мог бы работать и получше.

Иногда пауза оказывается слишком долгой, и тогда неплохо иметь возможность её отменить. Ладно, проблема в том, что GC — это компромисс множества параметров, один из которых — продолжительность паузы.

G1 действительно анализирует поведение приложения и выбирает фронт работ (выраженный в виде collection set) на основе своих умозаключений. Когда такое происходит? Иногда это занимает излишне много времени. Когда объем работ утверждён, G1 берётся собрать все живые объекты в collection set, упрямо и без остановок, за один присест. Его можно обдурить, внезапно поменяв поведение своего приложения так, что эвристика будет отрабатывать поверх протухших данных, когда в collection set попадет слишком много старых регионов. По сути, это означает, что G1 неправильно посчитал объем работ.

Кое-что инкрементально собирать не имеет смысла (молодые регионы), поэтому вся такая работа выделяется в "обязательный" блок, который таки выполняется непрерывно. Чтобы выйти из положения, G1 был доработан следующим механизмом: если эвристика регулярно выбирает неверный объем работ, G1 переходит на инкрементальную сборку мусора, шаг за шагом, и каждый следующий шаг (если он не влез в целевое время выполнения) можно отменить.

Ничего, нужно обновиться на JDK 12, всё станет лучше само собой. Что с этим делать конечному пользователю?

346: Promptly Return Unused Committed Memory from G1

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

В JDK 11 он отдаёт commited-память операционной системе только при full GC, или в ходе фазы параллельной маркировки. Чтобы достигнуть своей цели по допустимой длине паузы, G1 производит набор инкрементальных, параллельных и многоэтапных циклов. Если подключить логирование (-Xloggc:/home/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps), то эта фаза отображается как-то так:

8801.974: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 12582912000 bytes, allocation request: 0 bytes, threshold: 12562779330 bytes (45.00 %), source: end of GC]
8804.670: [G1Ergonomics (Concurrent Cycles) initiate concurrent cycle, reason: concurrent cycle initiation requested]
8805.612: [GC concurrent-mark-start]
8820.483: [GC concurrent-mark-end, 14.8711620 secs]

Наша ситуация, когда хип никто не трогает — это нечто прямо противоположное. Забавно то, что G1 как может борется с полными остановками, да и concurrent cycle запускает только при частых аллокациях и забитом хипе. Ситуации, когда G1 почешется отдать память в операционную систему будут происходить ну очень редко!

Глядите, какой крутой доклад, до краёв наполненный болью. Так бы все и забили на эту проблему ("купи ещё больше оперативки, чего как нищеброд!"), если бы не одно но — есть всякие облака и контейнеры, в которых это означает недостаточную утилизацию и потерю серьезных денег.

Нужно определять недостаточную утилизацию хипа и соответственно уменьшать его использование. Решением стало научить G1 хорошо вести себя в этом конкретном случае, как уже умеют Шенанда или GenCon из OpenJ9. На каких-то тестах на Томкате это позволило уменьшить расход памяти почти в два раза.

Как только что-то из этого произошло, запускается периодическая сборка мусора — она конечно, не почистит настолько же хорошо, как полная сборка, зато минимально затронет приложение. Суть в том, что приложение считается неактивным, или если истёк интервал (в миллисекундах) с последней сборки и нет concurrent cycle, или если getloadavg() на периоде в одну минуту показал нагрузку ниже определённого порога.

Можно повтыкать вот в этот лог:

(1) [6.084s][debug][gc,periodic ] Checking for periodic GC. [6.086s][info ][gc ] GC(13) Pause Young (Concurrent Start) (G1 Periodic Collection) 37M->36M(78M) 1.786ms
(2) [9.087s][debug][gc,periodic ] Checking for periodic GC. [9.088s][info ][gc ] GC(15) Pause Young (Prepare Mixed) (G1 Periodic Collection) 9M->9M(32M) 0.722ms
(3) [12.089s][debug][gc,periodic ] Checking for periodic GC. [12.091s][info ][gc ] GC(16) Pause Young (Mixed) (G1 Periodic Collection) 9M->5M(32M) 1.776ms
(4) [15.092s][debug][gc,periodic ] Checking for periodic GC. [15.097s][info ][gc ] GC(17) Pause Young (Mixed) (G1 Periodic Collection) 5M->1M(32M) 4.142ms
(5) [18.098s][debug][gc,periodic ] Checking for periodic GC. [18.100s][info ][gc ] GC(18) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 1.685ms
(6) [21.101s][debug][gc,periodic ] Checking for periodic GC. [21.102s][info ][gc ] GC(20) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.868ms
(7) [24.104s][debug][gc,periodic ] Checking for periodic GC. [24.104s][info ][gc ] GC(22) Pause Young (Concurrent Start) (G1 Periodic Collection) 1M->1M(32M) 0.778ms

Я — нет. Разобрались? В JEP есть и подробный сурдоперевод каждой строчки лога, и как работает алгоритм, и всё остальное.

Теперь у нас появились две дополнительные ручки: G1PeriodicGCInterval и G1PeriodicGCSystemLoadThreshold, которые можно крутить, когда станет плохо. "Ну и что, зачем я это узнал?" — спросите вы. Плохо ведь точно когда-нибудь станет, это Java, детка!

Ровно половина улучшений касаются производительности: три JEP-а про GC и один про CDS, которые обещают включиться сами собой, стоит только обновиться до JDK 12. В результате у нас на руках крепкий релиз — не революция, но эволюция, сфокусированная на улучшении перформанса. Кроме того, мы получили одну языковую фичу (switch expressions), два новых инструмента для разработчиков JDK (Constants API и тесты на JMH), и теперь сообщество может лучше сфокусироваться над одним-единственным 64-битным портом на ARM.

Она вам понадобится. В общем, переходите на JDK 12 сейчас, и да пребудет с вами Сила.

Совсем скоро, 5-6 апреля, пройдёт конференция JPoint, на которой соберётся огромное количество людей, знающих толк в JDK и всевозможных новых фичах. Минутка рекламы. Самое правильное место, чтобы обсудить свежий релиз! Например, точно будет Саймон Риттер из Азула с докладом «JDK 12: Pitfalls for the unwary». Подробней о JPoint можно узнать на официальном сайте.

Теги
Показать больше

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

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть