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

в 17:45, , рубрики: java, Блог компании JUG.ru Group, Компиляторы, Программирование

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

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

Как всё происходит

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

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

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

Баги! Паника! Все на дно!

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

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

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

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

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

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-океана — Марк Рейнхольд.

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

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

Тем не менее, таким образом можно посмотреть, сколько багов перенесено на 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 12! Обзор горячих JEP-ов - 4

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

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

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

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

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


230: Microbenchmark Suite

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

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

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

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

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

    public static final int LENGTH = 400;
    public static final int ARR_LEN = 100;
    public int largeLen = 100;
    public int smalllen = 6;

    @Benchmark
    public void testLargeConstArray(Blackhole bh) throws Exception {
        int localArrlen = ARR_LEN;
        for (int i = 0; i < LENGTH; i++) {
            Object[] tmp = new Object[localArrlen];
            bh.consume(tmp);
        }
    }

    //...
}


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;
    }
};

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

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

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


334: JVM Constants API

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

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

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

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

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


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

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

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

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


341: Default CDS Archives

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

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

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

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

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

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


344: Abortable Mixed Collections for G1

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

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

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

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

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

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


346: Promptly Return Unused Committed Memory from G1

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

Чтобы достигнуть своей цели по допустимой длине паузы, G1 производит набор инкрементальных, параллельных и многоэтапных циклов. В JDK 11 он отдаёт commited-память операционной системе только при full GC, или в ходе фазы параллельной маркировки. Если подключить логирование (-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 можно узнать на официальном сайте.

Автор: olegchir

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js