Русский
Русский
English
Статистика
Реклама

Java15

Как мы переходили на Java 15, или история одного бага в jvm длинной в 6 лет

12.02.2021 10:14:08 | Автор: admin

Мы готовились к выходу Java 15 ради некоторых её новых возможностей. В частности текстовых блоков. Да, они появились в Java 14 (о новых функциях в Java 14 можно посмотреть здесь), но только как превью-фича, а, начиная с Java 15, она стала доступна в виде окончательно готовой функции.

Мы в hh.ru привыкли внедрять и использовать самые современные технологии в разработке ПО. Пробовать что-то новое одна из ключевых задач команды Архитектура. Пока многие пишут на Java 8, мы уже близки к тому, чтобы отправить на свалку истории Java 11.

Как известно, Java стала выходить куда как чаще, поэтому работы по обновлению версий у нас прибавилось. С одной стороны, пришлось адаптироваться к новым реалиям, менять привычки, что не всегда комфортно. С другой можно заранее целиться в функции, которые появятся в новой версии языка и не ждать релиза 3 года.

Переезд с Java 14 на Java 15. Что-то пошло не так

Дождавшись выхода новый Java, мы приступили к переезду. Не мудрствуя лукаво, выбрали один из нагруженных сервисов, который уже крутился на Java 14. В теории никаких сложностей при переходе не должно было возникнуть, на практике так и получилось. Обновление Java 14 на Java 15 не тоже самое, что обновление Java 8 на Java 11.

hh и в продакшн сервис обновлён, работа выполнена. Что дальше? А дальше мониторинг работы. Для сбора метрик мы используем okmeter. С его помощью мы наблюдали за поведением обновленного сервиса. Никаких аномалий по сравнению с предыдущей версией Java не было, кроме одной нативная память. В частности, зона Code Cache выросла почти в 2 раза!

До конца 17 ноября Java 14, после Java 15До конца 17 ноября Java 14, после Java 15

Для сервиса с большим количеством инстансов каждый мегабайт памяти на счету, и, помимо её резкого роста, на графике просматривается возрастающий тренд. Кажется мы имеем дело с утечкой нативной памяти в Code Cache.

Что такое вообще этот ваш Code Cache?

Code Cache область нативной памяти, где хранится интерпретатор байткода Java, JIT-компиляторы C1 и C2, и оптимизированный ими код. Основным пользователем является JIT. Весь перекомпилированный им код будет сохранятся в Code Cache.

Начиная с Java 9 Code Cache поделен на три отдельных сегмента, в каждом из которых хранится свой тип оптимизированного кода (JEP 197). Но на графике выше видно только одну выделенную область, несмотря на то что там Java 14 и Java 15. Почему одну?

Дело в том, что мы тонко настраивали размеры памяти при переводе сервисов в Docker (о том, как это было, можно почитать тут) и умышленно установили флаг размера Code Cache (ReservedCodeCacheSize) равным 72МБ в этом сервисе.

Три сегмента можно получить двумя путями: оставить значение ReservedCodeCacheSize по умолчанию (256Мб) или использовать ключ SegmentedCodeCache. Вот как эти зоны выглядят на графике с другого нашего сервиса:

Поиск утечки нативной памяти в Code Cache

С чего начать расследование? Первое что приходит на ум использовать Native Memory Tracking, функцию виртуальной машины HotSpot, позволяющую отслеживать изменение нативной памяти по конкретным зонам. В нашем случае использовать Native Memory Tracking нет необходимости, так как благодаря собранным метрикам, мы уже выяснили, что проблема в Code Cache. Поэтому мы решаем сделать следующее запустить инстансы сервиса с Java 14 и Java 15 вместе. Так как у нас уже три дня сервис работает на "пятнашке", добавляем один инстанс на 14-ой.

Мы решаем продолжить поиск утечки с помощью утилит Java. Начнем с jcmd. Так как мы знаем, что "течет" у нас Code Cache, мы обращаемся к нему. Если сервис запущен в Docker, можно выполнить команду таким образом для каждого инстанса:

docker exec <container_id> jcmd 1 Compiler.CodeHeap_Analytics

Получаем два очень длинных и подробных отчета о состоянии Code Cache. Кропотливо сравнив их, мы обратили внимание на следующий интересный факт, связанный с очисткой Code Cache:

// Java 14Code cache sweeper statistics:Total sweep time: 9999 msTotal number of full sweeps: 17833Total number of flushed methods: 10681 (thereof 1017 C2 methods)Total size of flushed methods: 20180 kB// Java 15Code cache sweeper statistics:Total sweep time: 5592 msTotal number of full sweeps: 236 Total number of flushed methods: 11925 (thereof 1146 C2 methods)Total size of flushed methods: 44598 kB

Обратите внимание на количество циклов полной очистки Total number of full sweeps. Вспомним, что сервис на Java 15 работает 3 дня, а на Java 14 всего 20 минут. Но количество полных очисток Code Cache поразительно разнится почти 18 тысяч за 20 минут, против 236 за трое суток.

Как работает очистка Code Cache

Пришло время углубиться в детали. За очистку Code Cache отвечает отдельный поток jvm CodeCacheSweeperThread, который вызывается с определенной эвристикой. Поток реализован как бесконечный цикл while, внутри которого он блокируется, пока не истечет 24-часовой таймаут, либо не будет снята блокировка вызовом:

CodeSweeper_lock->notify();

После того, как блокировка снята, поток проверяет, истек ли таймаут и имеет ли хотя бы один из двух флагов, запускающих очистку Code Cache, значение true. Только при выполнении этих условий, поток вызовет очистку Code Cache методом sweep(). Давайте подробнее разберем флаги:

should_sweep. Этот флаг отвечает за две стратегии очистки Code Cache нормальную и агрессивную. О стратегиях поговорим дальше.

force_sweep. Этот флаг устанавливается в true при необходимости принудительно очистить Code Cache без выполнения условий нормальной и агрессивной стратегий очистки. Используется в тестовых классах jdk.

Нормальная очистка

  1. Во время вызова GC хранящиеся в Code Cache методы могут изменить свое состояние по следующему сценарию: alive -> notentrant -> zombie. Методы не-alive помечаются как "должны быть удалены из Code Cache при следующем запуске потока очистки".

  2. В конце своей работы GC передает ссылку на все не-alive объекты в метод report_state_change.

  3. Далее в специальную переменную bytes_changed инкрементируется суммарный размер объектов, помеченных как не-alive в этом проходе GC.

  4. Когда bytes_changed достигает порога, задаваемого в переменной sweep_threshold_bytes, флаг should_sweep помечается как true и блокировка потока очистки снимается.

  5. Запускается алгоритм очистки Code Cache, в начале которого значение bytes_changed сбрасывается. Сам он состоит из двух фаз: сканирование стека на наличие активных методов, удаление из Code Cache неактивных. На этом нормальная очистка завершена.

Начиная с Java 15 пороговым значением можно управлять с помощью флага jvm SweeperThreshold он принимает значение в процентах от общего количества памяти Code Cache, заданном флагом ReservedCodeCacheSize.

Агрессивная очистка

Этот тип очистки появился еще в Java 9, как один из способов борьбы с переполнением Code Cache. Выполняется в тот момент, когда свободного места в памяти Code Cache становится меньше заранее установленного процента. Этот процент можно установить самостоятельно, используя ключ StartAggressiveSweepingAt, по умолчанию он равен 10.

В отличие от нормальной очистки, где мы ждем наполнения буфера "мертвыми" методами, проверка на старт агрессивной очистки выполняется при каждой попытке аллокации памяти в Code Cache. Другими словами, когда JIT-компилятор хочет положить новые оптимизированные методы в Code Cache, запускается проверка на необходимость запуска очистки перед аллокацией. Проверка эта довольно простая, если свободного места меньше, чем указано в StartAggressiveSweepingAt, очистка запускается принудительно. Алгоритм очистки такой же, как и при нормальной стратегии. И только после выполнения очистки, JIT сможет положить новые методы в Code Cache.

Что у нас?

В нашем случае размер Code Cache был ограничен 72 МБ, а флаг StartAggressiveSweepingAt мы не задавали, значит по умолчанию он равен 10. Если взглянуть на статистику очистки Code Cache, может показаться, что на Java 14 работает именно агрессивная стратегия. Дополнительно убедиться в этом нам помог тот же график, но с увеличенным масштабом:

Java 14Java 14

Он имеет зубчатую структуру, которая говорит о том, что очистка происходит часто, и, вероятно, методы по кругу выгружаются из Code Cache, в следующей итерации JIT-компиляции вновь попадают обратно, после снова удаляются etc.

Но как это возможно? Почему работает агрессивная стратегия очистки? По умолчанию она должна запускаться в тот момент, когда свободного места в Code Cache менее 10%, в нашем случаем только при достижении 65 мегабайт, но мы видим, что она происходит и при 30-35 мегабайтах занятой памяти.

Для сравнения, график с запущенной Java 15 выглядит иначе:

Java 15Java 15

Зубчатая структура отсутствует, есть плавный рост, затем очистка и снова рост. Разгадка где-то рядом.

Утечка не утечка

Так как работой Code Cache управляет jvm, мы отправились искать ответы в исходниках openJDK, сравнивая версии Java 14 и Java 15. В процессе поисков мы обнаружили интересный баг. Там сказано, что агрессивная очистка Code Cache работает неправильно с того момента, как ее внедрили в Java 9. Вместо старта агрессивной очистки при 10% свободного места, она вызывалась при 90% свободного места, то есть почти всегда. Другими словами, оставляя опцию StartAggressiveSweepingAt = 10, на деле мы оставляли StartAggressiveSweepingAt = 90. Баг был исправлен 3 июля 2020 года. А все дело было в одной строчке:

Этот фикс вошел во все версии Java после 9-ки. Но почему тогда его нет в нашей Java 14? Оказывается, наш docker-образ Java 14 был собран 15 апреля 2020 года, и тогда становится понятно, почему фикс туда не вошел:

Так значит и утечки нативной памяти в Code Cache нет? Просто всё время очистка работала неправильно, впустую потребляя ресурсы cpu. Понаблюдав еще несколько дней за сервисом на Java 15, мы сделали вывод, что так и есть. Общий график нативной памяти вышел на плато и перестал показывать тренд к росту:

скачок на графике - это переход на java 15скачок на графике - это переход на java 15

Выводы

  1. Как можно чаще обновляйте свою Java. Это касается не только мажорных версий, но и патчевых. Там могут содержаться важные фиксы

  2. Разумное использование метрик помогает обнаружить потенциальные проблемы и аномалии

  3. Переходите на Java 15, оно того стоит. Вот тут список всех фич, которые появились в пятнашке

  4. Если вы используете Java 8, то у вас проблемы агрессивной очистки Code Cache нет, за отсутствием этого функционала как такового. Однако существует риск, что Code Cache может переполниться и JIT-компиляция будет принудительно отключена

Подробнее..

Вышла Java 16

16.03.2021 18:09:12 | Автор: admin

Вышла 16-я версия платформы Java SE. В этот релиз попало около двух с половиной тысяч закрытых задач и 17 JEP'ов. Изменения API можно посмотреть здесь. Release notes здесь.


Уже сейчас доступны для скачивания дистрибутивы Oracle JDK и OpenJDK.



JEP'ы, которые попали в Java 16, мы разобьём на четыре категории: язык, API, JVM и инфраструктура.


Язык


Паттерн-матчинг для оператора instanceof (JEP 375)


Оператор instanceof с паттерн-матчингом, который появился в Java 14 и перешёл во второе preview в Java 15, теперь стал стабильной синтаксической конструкцией и больше не требует флага --enable-preview. Паттерн-матчинг мы подробно рассматривали в этой статье, и с того момента в него было внесено два изменения:


Во-первых, переменные паттернов теперь не являются неявно финальными:


if (obj instanceof String s) {    s = "Hello"; // OK в Java 16, ошибка в Java 15}

Во-вторых, если тип выражения, известный на этапе компиляции, является подтипом проверяемого типа, то теперь это ошибка компиляции:


String str = ...if (str instanceof String s) { // Oшибка в Java 16, OK в Java 15}


Записи (JEP 395)


Ещё одна синтаксическая конструкция, которая стала стабильной это записи. Она также была в режиме preview в Java 14 и Java 15. Записи мы также подробно рассматривали ранее. В Java 16 было внесено следующее изменение: теперь во внутренних классах разрешено объявлять статические члены:


public class Outer {    public class Inner {        // OK в Java 16, ошибка в Java 15        static void main(String[] args) {        }        // OK в Java 16, ошибка в Java 15        record Point(int x, int y) {        }    }}


sealed классы (второе preview) (JEP 397)


Запечатанные классы, которые появились в Java 15 в режиме preview, остаются в этом статусе. Их мы рассматривали в этой статье. Изменения по сравнению с прошлой версией следующие:


  • Теперь в спецификации языка Java появилось понятие contextual keyword взамен старым понятиям restricted keyword и restricted identifier, и одними из таких contextual keywords стали sealed, non-sealed и permits.
  • Компилятор теперь производит более строгие проверки при конверсии типов, в иерархиях которых есть sealed классы:
    sealed interface Sealed {}final class Impl implements Sealed {    void f(Runnable r) {        Sealed s = (Sealed) r; // error: incompatible types    }}
    
  • Метод Class.permittedSubclasses() переименован в Class.getPermittedSubclasses().


JVM


Строгая инкапсуляция внутренностей JDK по умолчанию (JEP 396)


Инкапсуляция внутренних API JDK, которая была введена в Java 9, теперь стала строгой: если в Java 9-15 значение опции --illegal-access было по умолчанию permit, то с Java 16 она становится deny. Это значит, что рефлективный доступ к защищённым членам классов и статический доступ к неэкспортированным API (sun.*, com.sun.*, jdk.internal.* и т.д.) теперь будет выбрасывать ошибку.


Если код требует доступа к внутренностям JDK во время выполнения, то чтобы он продолжал работать на Java 16, теперь придётся явно указывать одну из трёх опций JVM:


  • --illegal-access=permit/warn/debug: открытие всех пакетов JDK
  • --add-opens=module/package=target-module: открытие одного пакета
  • --add-exports=module/package=target-module: экспортирование одного пакета (только для статического доступа)

В будущем опция --illegal-access может быть удалена окончательно. Начиная с Java 16, при её использовании выдаётся предупреждение: Option --illegal-access is deprecated and will be removed in a future release.


Изменения не касаются критического API в модуле jdk.unsupported: классы в пакетах sun.misc и sun.reflect остаются доступными без флагов.



Warnings for Value-Based Classes (JEP 390)


Классы-обёртки примитивных типов (Integer, Double, Character и т.д.) теперь относятся к категории value-based классов, и их конструкторы, которые ранее стали deprecated в Java 9, теперь помечены как deprecated for removal.


Понятие value-based классов появилось в спецификации API Java 8. Такие классы являются неизменяемыми, создаются только через фабрики, и в их использовании не должны использоваться операции, чувствительные к identity: сравнение на ==, синхронизация, identityHashCode() и т.д. Value-based классы являются кандидатами для миграции на примитивные классы в рамках проекта Valhalla, который сейчас находится в стадии активной разработки.


При синхронизации на объектах value-based классов теперь будет выдаваться предупреждение во время компиляции:


Double d = 0.0;synchronized (d) { // warning: [synchronization] attempt to synchronize on an instance of a value-based class}

Также можно включить проверки синхронизации на value-based объектах во время выполнения с помощью флагов JVM:


  • -XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=1: при попытке синхронизации будет фатальная ошибка.
  • -XX:+UnlockDiagnosticVMOptions -XX:DiagnoseSyncOnValueBasedClasses=2: при попытке синхронизации будет предупреждение.


ZGC: Concurrent Thread-Stack Processing (JEP 376)


Обработка стеков потоков в сборщике мусора ZGC теперь перенесена из safepoints в конкурентную фазу. Это позволило ещё сильнее уменьшить паузы сборщика мусора.



Unix-Domain Socket Channels (JEP 380)


Добавлена поддержка сокетов доменов Unix в socket channel и server-socket channel API. Такие сокеты используются для межпроцессного взаимодействия внутри одного хоста, и в них не используются сетевые соединения, что делает такое взаимодействие более безопасным и эффективным. Сокеты доменов Unix с недавних пор поддерживаются в Windows 10 и Windows Server 2019.



Elastic Metaspace (JEP 387)


Metaspace (пространство JVM, в котором хранятся метаданные классов) переработан для более эффективной отдачи неиспользуемой памяти обратно операционной системе и меньшего потребления памяти вне кучи в целом. Такое улучшение может быть полезно для приложений, которые интенсивно загружают и выгражают классы посредством большого количества загрузчиков классов.



Alpine Linux Port (JEP 386)


JDK теперь портирован на Alpine Linux и другие дистрибутивы Linux, которые используют musl в качестве реализации стандартной библиотеки C. Alpine Linux популярен в облаках, микросервисах и контейнерах благодаря своему маленькому размеру образа. Новый порт позволит нативно запускать JDK в этих окружениях.



Windows/AArch64 Port (JEP 388)


JDK также портирован на архитектуру Windows/AArch64. Это позволит запускать Java на компьютерах с Windows on ARM, которые в последнее время набирают популярность.



API


Новые методы в Stream


Хотя для этих двух новых методов в интерфейсе java.util.stream.Stream нет отдельного JEP, хочется упомянуть их здесь, так как это довольно заметное изменение.


Первый метод это Stream.toList(). Этот метод собирает содержимое Stream в неизменяемый список и возвращает его. При этом, в отличие от Collectors.toUnmodifiableList(), список, который возвращается из Stream.toList(), толерантен к null-элементам.


Второй метод это Stream.mapMulti() (и примитивные специализации). Это метод является императивным аналогом метода Stream.flatMap(): если flatMap() принимает функцию, которая для каждого элемента должна вернуть Stream, то mapMulti() принимает процедуру с двумя параметрами, где первый параметр это текущий элемент, а второй Consumer, в который кладутся значения. Пример:


IntStream.rangeClosed(1, 10).mapMulti((i, consumer) -> {    for (int j = 1; j <= i; j++) {        consumer.accept(j);    }}); // Возвращает 1, 1, 2, 1, 2, 3, 1, 2, 3, 4, 1, 2, 3, 4, 5, ...


Инструмент упаковки (JEP 392)


Инструмент создания самодостаточных приложений jpackage, который появился в Java 14 в инкубационном статусе, теперь стал постоянным модулем.



Vector API (Incubator) (JEP 338)


Появился новый инструментарий для преобразования векторных вычислений в SIMD-инструкции процессора (x64 и AArch64). Векторное API позволит разработчику контролировать процесс компиляции и не полагаться на автовекторизацию, которая в JVM является ограниченным и хрупким механизмом. Явная векторизация может применяться в таких областях как машинное обучение, линейная алгебра, криптография и др.


API находится в инкубационном модуле jdk.incubator.vector.



Foreign Linker API (Incubator) (JEP 389)


Ещё одно новое API, которое появилось в результате работы над проектом Panama это Foreign Linker API. Это инструментарий для статического доступа к нативному коду из Java, созданный для замены JNI: он должен быть более простым в использовании, более безопасным и желательно более быстрым.


Про Foreign API делал доклад Владимир Иванов из Oracle.



Foreign-Memory Access API (Third Incubator) (JEP 393)


API для доступа вне кучи Java, которое появилось в Java 14, остаётся в инкубационном статусе с некоторыми изменениями.



Инфраструктура


Enable C++14 Language Features (JEP 347)


Кодовая база JDK до Java 16 использовала стандарты C++98/03. При этом с Java 11 код стал собираться версией с более новым стандартом, однако в нём всё ещё нельзя было использовать возможности стандарта C++11/14. Теперь же часть из этих возможностей использовать можно: в гиде по стилю HotSpot определён список возможностей C++11/14, которые можно использовать и которые нельзя.



Migrate from Mercurial to Git (JEP 357) и Migrate to GitHub (JEP 369)


Совершён переход репозиториев JDK на Git и GitHub. Миграция была полностью завершена в сентябре 2020 года, и разработка Java 16 уже полностью велась в новом репозитории.


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


Также сейчас обсуждается переход на Git более старых версий JDK: jdk11u и, возможно, jdk8u.



Java 16 является STS-релизом, у которого выйдет только два обновления.



Если вы не хотите пропускать новости о Java, то подписывайтесь на Telegram-канал miniJUG

Подробнее..

Вышла Java 15

15.09.2020 18:11:52 | Автор: admin

Сегодня в свет вышла новая, 15-я версия платформы Java.


Скачать JDK 15 можно по следующим ссылкам:


  • Oracle JDK (проприетарная версия, обратите внимание на ограничения в использовании).
  • OpenJDK (бесплатная версия)

В новый релиз попало 14 JEP'ов и сотни более мелких улучшений. Если хочется ознакомиться с полным списком изменений с точностью до всех JIRA-тикетов, то их можно посмотреть на сайте Алексея Шипилёва. Также если интересны все изменения API, то их можно посмотреть здесь.



Перечислим JEP'ы, которые попали в Java 15:


Язык


Блоки текста (JEP 378)


Блоки текста, которые появились в Java 13 и прошли два preview, теперь стали стабильной синтаксической конструкцией. Это значит, что в Java теперь две постоянные конструкции, которые появились с выхода Java 11: выражения switch и блоки текста.


Паттерн-матчинг для оператора instanceof (второе preview) (JEP 375)


Улучшенный оператор instanceof, который появился в Java 14, перешёл во второе preview без изменений. Напомним, что режим preview существует в Java для нововведений, которые находятся в предварительном статусе, т.е. могут измениться несовместимым образом или даже совсем исчезнуть, и для их включения необходим специальный флаг --enable-preview. Паттерн-матчинг для instanceof мы подробно рассматривали в этой статье.


Записи (второе preview) (JEP 384)


Записи, которые также появились в Java 14, тоже остались в режиме preview. Изменений по сравнению с прошлой версией немного: убрано ограничение, что канонический конструктор должен быть public, а также разрешены локальные перечисления и интерфейсы.


Sealed классы (preview) (JEP 360)


В Java появилось языковое нововведение: запечатанные классы. Помечаются такие классы модификатором sealed, после чего круг классов, которые могут наследоваться от данного класса, становится ограниченным. sealed классы мы подробно рассматривали в этой статье.


JVM


ZGC (JEP 377)


ZGC, который появился в Java 11 в экспериментальном статусе, теперь официально готов к продуктовой разработке. Напомним, что ZGC это сборщик мусора, который нацелен на маленькие паузы (< 10мс) и готовность работать в условиях огромных куч (> 1TB).


Shenandoah (JEP 379)


Shenandoah, ещё один низкопаузный сборщик мусора и конкурент ZGC, теперь также имеет статус готового к продуктовой разработке. Shenandoah впервые появился в Java 12. Также недавно стало известно, что Shenandoah был бэкпортирован в JDK 11, который является текущим LTS-релизом Java. Это значит, что чтобы его использовать, необязательно обновляться до JDK 15, а достаточно обновиться до JDK 11.0.9, которая выйдет 20 октября 2020 года.


Disable and Deprecate Biased Locking (JEP 374)


Biased Locking, который много лет существовал в JDK, было решено убрать из-за сложности поддержки и неочевидных преимуществ этой оптимизации. Начиная с этого релиза, опция -XX:+UseBiasedLocking отключена по умолчанию, а при её использовании и всех её связанных опций будет выдаваться предупреждение. Про мотивы отключения Biased Locking рассказал Сергей Куксенко в подкасте Hydra.


Удаление портов Solaris и SPARC (JEP 381)


Порты JDK на Solaris/SPARC, Solaris/x64 и Linux/SPARC, которые стали deprecated for removal в Java 14, теперь удалены окончательно. Удаление этих портов упростит и ускорит разработку JDK.


API


Скрытые классы (JEP 371)


Появился новый тип классов, называемых скрытыми. На скрытые классы не могут прямо ссылаться другие классы, и всё их использование может осуществляться только через рефлексию. Также их нельзя обнаружить по имени, и их методы не появляются в стек-трейсах. Создаются такие классы с помощью нового метода Lookup.defineHiddenClass().


Удаление движка JavaScript Nashorn (JEP 372)


Движок Nashorn, который стал deprecated for removal в Java 11, теперь удалён окончательно. В качестве замены Nashorn теперь придётся искать другой движок JavaScript, например, GraalVM JavaScript или Rhino.


Reimplement the Legacy DatagramSocket API (JEP 373)


Реализации старых сокетов из JDK 1.0 java.net.DatagramSocket and java.net.MulticastSocket были полностью заменены на более простые, современные и легкоадаптируемые к виртуальным нитям, которые планируется ввести в язык в рамках проекта Loom. Ранее в Java 13 были переписаны java.net.Socket и java.net.ServerSocket.


Foreign-Memory Access API (Second Incubator) (JEP 383)


API для доступа вне кучи Java, которое появилось в Java 14 в статусе модуля-инкубатора, остаётся в этом статусе.


Deprecate RMI Activation for Removal (JEP 385)


Устаревшая и малоиспользуемая часть RMI, которая называется RMI Activation, стала deprecated for removal.


Edwards-Curve Digital Signature Algorithm (EdDSA) (JEP 339)


Современный алгоритм с открытым ключом для создания цифровой подписи EdDSA реализован в Java.


Java 15, как и 12, 13, 14, является STS-релизом, и у неё выйдет только два обновления.

Подробнее..
Категории: Java , Graalvm , Java15 , Java14 , Java13 , Java12 , Sealed , Nashorn , Zgc , Shenandoah

API, ради которых наконец-то стоит обновиться с Java 8. Часть 3

24.04.2021 18:16:03 | Автор: admin

Какие есть причины переходить на новые версии Java? Кто-то это сделает из-за новых языковых возможностей вроде выражений switch, блоков текста или записей. Кому-то понадобятся новые интересные возможности вроде модулей или низкопаузных сборщиков мусора. Кто-то это сделает просто из-за того, что обновив версию Java, их программа станет быстрее и будет есть меньше памяти. Но есть ещё одна, не менее важная причина. Это новые API, которые позволят писать меньше кода и избежать траты времени на поиск нужной функциональности во внешних библиотеках. А в некоторых случаях сделают ваш код быстрее.


В предыдущих двух частях мы уже рассмотрели по 10 новых API, которые появились в Java 9 и более поздних версиях (часть 1, часть 2). Сегодня мы рассмотрим ещё 10.




1. Stream.toList()


Появился в: Java 16


Для какой задачи чаще всего используется Stream в Java? Конечно же, для трансформации списков: у нас есть список, над которым нужно совершить какое-то преобразование и вернуть новый. Такой паттерн вы наверняка видели в своём проекте множество раз:


List<T> targetList = sourceList        .stream()        // промежуточные операции        .collect(Collectors.toList());

Нельзя сказать, что collect(Collectors.toList()) является очень уж громоздкой конструкцией, но всё же хочется для такой частой операции писать поменьше кода. И в Java 16 это стало возможно с помощью нового метода Stream.toList():


List<T> targetList = sourceList        .stream()        // промежуточные операции        .toList();

Есть ли какая-то разница между toList() и collect(Collectors.toList())? Оба ли способа ведут себя одинаково? С практической точки зрения можно сказать, что нет: если toList() возвращает неизменяемый список, то collect(Collectors.toList()) возвращает некий список, о котором неизвестно, неизменяемый он или нет. То есть если вы нигде в коде не используете негарантированную вам спецификацией изменяемость списка (надеюсь, вы из такой категории людей), то смело можете заменять collect(Collectors.toList()) на toList().


Однако Stream.toList() делает код не только короче, но и эффективнее! Дело в том, что Stream.toList() использует внутри себя Stream.toArray(), который выделяет массив точной длины, если Spliterator имеет характеристику SIZED. В то время как Collectors.toList() никак эту характеристику не использует и всегда начинает с пустого ArrayList, накапливая в нём элементы с постоянными переаллокациями.


Давайте напишем несколько бенчмарков. Для начала рассмотрим самый простейший случай: замерим, как быстро создаётся копия исходного списка, т.е. проверим цепочку вообще без промежуточных операций. Так как для такого сценария Stream по идее вообще не нужен, и того же самого можно добиться просто вызвав new ArrayList<>(sourceList) или List.copyOf(sourceList), то замерим и эти два случая тоже:


Полный код JMH-бенчмарка
import org.openjdk.jmh.annotations.*;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;import java.util.stream.Collectors;import java.util.stream.IntStream;@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)@State(Scope.Thread)public class ToList {    @Param({"10", "100", "1000"})    private int size;    private List<Integer> sourceList;    @Setup    public void setup() {        sourceList = IntStream                .range(0, size)                .boxed()                .collect(Collectors.toList());    }    @Benchmark    public List<Integer> newArrayList() {        return new ArrayList<>(sourceList);    }    @Benchmark    public List<Integer> toList() {        return sourceList.stream().toList();    }    @Benchmark    public List<Integer> copyOf() {        return List.copyOf(sourceList);    }    @Benchmark    public List<Integer> collectToList() {        return sourceList.stream().collect(Collectors.toList());    }    @Benchmark    public List<Integer> collectToUnmodifiableList() {        return sourceList.stream().collect(Collectors.toUnmodifiableList());    }}

Детали запуска бенчмарка
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)Intel Core i5-9500 3.00GHZОпции запуска: -f 3 -wi 3 -w 5 -i 5 -r 5 -t 6 -jvmArgs -XX:+UseParallelGC


Результаты говорят нам о том, что Stream.toList() не только существенно быстрее collect(Collectors.toList()), но и может быть быстрее даже List.copyOf()! Это объясняется тем, что в List.copyOf() существенное время тратится на проверку requireNonNull для каждого входного элемента, поскольку он запрещает null-элементы, в то время как Stream.toList() не запрещает null и в нём такая проверка не нужна. На малых размерах List.copyOf() всё же выигрывает, потому что проверка нескольких элементов на null быстрее церемоний, которые есть у Stream: создание Spliterator, ReferencePipeline и т.д.


Теперь рассмотрим случай, когда точный размер неизвестен. Например, добавим одну промежуточную операцию filter():


Полный код JMH-бенчмарка
import org.openjdk.jmh.annotations.*;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;import java.util.stream.Collectors;import java.util.stream.IntStream;@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)@State(Scope.Thread)public class ToListFilter {    @Param({"10", "100", "1000"})    private int size;    private List<Integer> sourceList;    @Setup    public void setup() {        sourceList = IntStream                .range(0, size)                .boxed()                .collect(Collectors.toList());    }    @Benchmark    public List<Integer> toList() {        return sourceList.stream().filter(i -> i % 2 == 0).toList();    }    @Benchmark    public List<Integer> newArrayList() {        var list = new ArrayList<Integer>();        for (var i : sourceList) {            if (i % 2 == 0) {                list.add(i);            }        }        return list;    }    @Benchmark    public List<Integer> collectToList() {        return sourceList.stream().filter(i -> i % 2 == 0).collect(Collectors.toList());    }    @Benchmark    public List<Integer> collectToUnmodifiableList() {        return sourceList.stream().filter(i -> i % 2 == 0).collect(Collectors.toUnmodifiableList());    }}

Детали запуска бенчмарка
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)Intel Core i5-9500 3.00GHZОпции запуска: -f 3 -wi 3 -w 5 -i 5 -r 5 -t 6 -jvmArgs -XX:+UseParallelGC



В этом случае мы тоже получили большое ускорение! И в этот раз Stream.toList() на большом количестве элементов выиграл даже у простого new ArrayList() с последующим заполнением в цикле. Как так получается? Дело в том, что при неизвестном размере Stream.toArray() использует структуру данных SpinedBuffer, которая более эффективна для накопления элементов, чем ArrayList. Она представляет собой массив массивов, где подмассивы имеют длины в виде возрастающих степеней двойки (каждый следующий chunk в два раза больше предыдущего). Структуру SpinedBuffer можно легко понять из этого рисунка, где изображён буфер со 100 элементами (Integer от 0 до 99):



Также ArrayList проигрывает ещё и потому, что расширяется всего лишь в 1.5 раза при заполнении внутреннего массива, а значит делает это намного чаще, что приводит к дополнительным накладным расходам. ArrayList вынужден балансировать между экономией памяти и скоростью операции add(), поэтому и проигрывает SpinedBuffer, который заточен исключительно на скорость. Излишний расход памяти для него не играет роли, так как это недолгоживующая структура, которая сразу же отбрасывается после завершения терминальной операции.


Вывод: Stream.toList() короче, чище и почти всегда быстрее, чем collect(Collectors.toList()). Так что о втором способе можно забыть и использовать всегда первый (хотя Collectors.toList() сам по себе всё ещё может быть нужен, например как downstream Collector для других Collector'ов). Если же нужен гарантированно изменяемый список, то можно использовать collect(Collectors.toCollection(ArrayList::new)).



2. String: formatted(), stripIndent() и translateEscapes()


Появились в: Java 15


В Java 15 появились блоки текста строковые литералы, которые могут состоять из одной или нескольких линий:


String str = """        Привет,        Юзер!""";

При этом довольно часто блоки будут использоваться в качестве шаблонов с последующей заменой:


String str = String.format("""        Привет,        %s!""", user);

Не кажется ли вам, что код выше выглядит несколько громоздким? Мне вот тоже кажется. Но у нас есть способ, как сделать его немножко чище. Это новый метод String.formatted(), который является нестатическим эквивалентом String.format():


String str = """        Привет,        %s!""".formatted(user);

Вы, конечно, скажете, что разница лишь в трёх символах сэкономленного кода, но, во-первых, второй вариант гораздо проще печатать на клавиатуре (проверьте сами), а во-вторых, он более читабелен.


Кстати, formatted() никто не запрещает использовать и с обычными литералами:


String str = "Привет, %s!".formatted(user);

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



Второй метод, появившийся в Java 15 это String.stripIndent(), который удаляет общие пробельные символы в начале всех линий. К примеру, если есть файл hello.txt с такими строками:


    Привет,    Юзер!

Тогда чтобы убрать пробелы слева, как раз и можно воспользоваться stripIndent():


String str = Files.readString(Path.of("hello.txt")).stripIndent();System.out.println(str);

Вывод:


Привет,Юзер!


Наконец, третий метод это String.translateEscapes(). Он делает простую вещь: заменяет экранирующие последовательности на их соответствующие символы.


Например, есть файл hello.txt:


Привет,\nЮзер!

String str = Files.readString(Path.of("hello.txt")).translateEscapes();System.out.println(str);

Вывод:


Привет,Юзер!


3. CharSequence.isEmpty(), CharSequence.compare() и StringBuilder.compareTo()


Появились в: Java 15 / Java 11


Если уж мы начали тему строк, то давайте добьём её до конца.


Тот, кто писал на Java 1.5 или на более старых версиях, должен помнить, что в классе String в те времена не было метода isEmpty(). Поэтому для проверки строки на пустоту каждый раз приходилось использовать length():


if (str.length() != 0) {    ...}

Это было не совсем удобно, и в Java 1.6 метод String.isEmpty() наконец-то завезли:


if (!str.isEmpty()) {    ...}

Однако про то, что String далеко не единственная реализация CharSequence (хоть и самая популярная), почему-то не подумали, и этот метод туда добавлять не стали (впрочем, без default-методов они бы этого сделать и не смогли). Например, для проверки на пустоту StringBuilder всё ещё приходилось использовать length():


if (stringBuilder.length() != 0) {    ...}

Но спустя 14 лет всё-таки решили исправить и это: начиная с Java 15, метод isEmpty() есть не только у String, но и у любой CharSequence:


if (!stringBuilder.isEmpty()) {    ...}


Также иногда приходится тестировать два CharSequence на равенство. Метод equals() использовать нельзя: а вдруг он не переопределён в реализации? Поэтому приходится изворачиваться: либо конвертировать каждый CharSequence в String и сравнивать их, что может быть накладно, либо писать свою реализацию посимвольного сравнения.


Однако, начиная с Java 11, всё это не нужно, потому что появился метод CharSequence.compare():


if (CharSequence.compare(charSeq1, charSeq2) == 0) {    ...}

Метод compare() также можно использовать не только для просто теста на равенство, но и для лексикографического сравнения.


Также в Java 11 класс StringBuilder стал реализовывать интерфейс Comparable, а значит для сравнения двух StringBuilder можно использовать compareTo():


if (stringBuilder1.compareTo(stringBuilder2) == 0) {    ...}


4. Collectors.filtering() и Collectors.flatMapping()


Появились в: Java 9


Часто ли вам приходится использовать Collectors.groupingBy()? К примеру, вы ведёте базу данных фильмов:


record Movie(String title, String genre, double rating) {}

Допустим, вы хотите сгруппировать фильмы по жанру:


Stream<Movie> allMovies = Stream.of(    new Movie("Коммандо", "Боевик", 7.385),    new Movie("Терминатор", "Боевик", 7.974),    new Movie("Терминатор 2", "Боевик", 8.312),    new Movie("Молчание ягнят", "Триллер", 8.33),    new Movie("Криминальное чтиво", "Триллер", 8.619),    new Movie("Титаник", "Мелодрама", 8.363),    new Movie("Семьянин", "Комедия", 7.699));Map<String, List<Movie>> groups = allMovies.collect(    Collectors.groupingBy(Movie::genre));groups.forEach((genre, movies) -> {    System.out.println(genre + ":");    movies.forEach(movie ->        System.out.printf("    %s: %.2f%n", movie.title(), movie.rating()));});

Вывод:


Мелодрама:    Титаник: 8.36Боевик:    Коммандо: 7.39    Терминатор: 7.97    Терминатор 2: 8.31Триллер:    Молчание ягнят: 8.33    Криминальное чтиво: 8.62Комедия:    Семьянин: 7.70

Однако, допустим, вы не хотите видеть все фильмы, а только те, у кого рейтинг выше 8. Какой метод вы в этом случае используете? Конечно же, Stream.filter():


Map<String, List<Movie>> groups = allMovies    .filter(movie -> movie.rating() > 8)    .collect(Collectors.groupingBy(Movie::genre));

Мелодрама:    Титаник: 8.36Боевик:    Терминатор 2: 8.31Триллер:    Молчание ягнят: 8.33    Криминальное чтиво: 8.62

Но вот проблема: вам вдруг захотелось видеть все жанры, даже те, в которые не попало ни одного фильма с рейтингом выше 8. Что делать? Ответ: перейти на новую версию Java, потому что в ней есть Collectors.filtering():


Map<String, List<Movie>> groups = allMovies.collect(    Collectors.groupingBy(Movie::genre,        Collectors.filtering(movie -> movie.rating() > 8,            Collectors.toList())));groups.forEach((genre, movies) -> {    System.out.println(genre + ":");    if (movies.isEmpty()) {        System.out.println("    <Фильмов с рейтингом выше 8 нет>");    } else {        movies.forEach(movie ->            System.out.printf("    %s: %.2f%n", movie.title(), movie.rating()));    }});

В этом случае фильтрация будет перенесена внутрь groupingBy(), и потери жанров не произойдёт:


Мелодрама:    Титаник: 8.36Боевик:    Терминатор 2: 8.31Триллер:    Молчание ягнят: 8.33    Криминальное чтиво: 8.62Комедия:    <Фильмов с рейтингом выше 8 нет>


Очень хорошо. Теперь добавим в фильмы актёров:


record Movie(String title, String genre, double rating, List<String> actors) {}

И теперь хотите увидеть всех актёров с группировкой по жанру:


Stream<Movie> allMovies = Stream.of(    new Movie("Коммандо", "Боевик", 7.385,        List.of("Шварценеггер", "Чонг", "Хедайя")),    new Movie("Терминатор", "Боевик", 7.974,        List.of("Шварценеггер", "Бин", "Хэмилтон")),    new Movie("Терминатор 2", "Боевик", 8.312,        List.of("Шварценеггер", "Хэмилтон", "Ферлонг", "Патрик")),    new Movie("Молчание ягнят", "Триллер", 8.33,        List.of("Фостер", "Хопкинс")),    new Movie("Криминальное чтиво", "Триллер", 8.619,        List.of("Траволта", "Уиллис", "Джексон", "Турман")),    new Movie("Титаник", "Мелодрама", 8.363,        List.of("ДиКаприо", "Уинслет")),    new Movie("Семьянин", "Комедия", 7.699,        List.of("Кейдж", "Леони")));

Но какой коллектор нужно подсунуть в groupingBy(), чтобы собрать всех актёров в Set? Можно попробовать Collectors.mapping():


Map<String, Set<List<String>>> groups = allMovies.collect(    Collectors.groupingBy(Movie::genre,        Collectors.mapping(Movie::actors, Collectors.toSet())));

Но смотрите, у нас получилось множество списков, а нужно просто множество. Что же делать? И тут на помощь приходит Collectors.flatMapping(), ещё один новый метод, появившийся в Java 9:


Map<String, Set<String>> groups = allMovies.collect(    Collectors.groupingBy(Movie::genre,        Collectors.flatMapping(movie -> movie.actors().stream(),            Collectors.toSet())));

И вот сейчас тип правильный! Если вывести это, то получится:


Мелодрама:    ДиКаприо    УинслетБоевик:    Бин    Ферлонг    Хедайя    Патрик    Шварценеггер    Хэмилтон    ЧонгТриллер:    Траволта    Уиллис    Хопкинс    Фостер    Джексон    ТурманКомедия:    Кейдж    Леони

Что и требовалось.



5. StackWalker


Появился в: Java 9


Приходилось ли вам иметь дело со стеками? Не со стеками в смысле структур данных, а со стеком потоков? Например, вы пишете простенький логгер:


public final class MyLogger {    public static void log(String message) {        System.out.println(message);    }}

Однако вы хотите писать в консоль не просто голое сообщение, а ещё имя класса, метода, файла и номер строки, откуда вызывается метод log(). В Java 8 единственным стандартным способом для этого является получение массива StackTraceElement[], например, с помощью метода Thread.getStackTrace():


public static void log(String message) {    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();    StackTraceElement stackTraceElement = stackTrace[2];    String msg = stackTraceElement.getClassName() + "."               + stackTraceElement.getMethodName() + "("               + stackTraceElement.getFileName() + ":"               + stackTraceElement.getLineNumber() + ") "               + message;    System.out.println(msg);}

Можно предположить, что такой способ получения получения номеров строк является довольно дорогим. Ведь нам надо заполнить полностью весь стек, который может быть очень глубоким (особенно в энтерпрайзе, где фреймворк на фреймворке), а потом ещё и сконвертировать внутренние структуры JVM в Java-массив. И всё это ради того, чтобы отбросить его почти полностью и достать только второй элемент. Давайте замерим производительность такого подхода:


@Benchmarkpublic String stackTrace() {    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();    StackTraceElement stackTraceElement = stackTrace[2];    return stackTraceElement.getClassName() + "."         + stackTraceElement.getMethodName() + "("         + stackTraceElement.getFileName() + ":"         + stackTraceElement.getLineNumber() + ")";}

Детали запуска бенчмарка
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)Intel Core i5-9500 3.00GHZОпции запуска: -f 1 -wi 3 -w 3 -i 5 -r 5 -t 6

Benchmark          Mode  Cnt    Score   Error  UnitsStack.stackTrace   avgt    5  103,704 ? 1,123  us/op

104 микросекунды на каждый вызов! Это невероятно медленно! Есть ли возможность это ускорить? Есть: с помощью нового класса StackWalker, который появился в Java 9. Давайте рассмотрим этот класс поподробнее.


StackWalker предоставляет возможность гулять по стеку. Чтобы это сделать, сначала нужно получить экземпляр StackWalker с помощью метода StackWalker.getInstance(). Этот метод возвращает StackWalker по умолчанию. Также есть возможность передать в метод getInstance() опции и получить StackWalker с более богатыми возможностями. Но об этом чуть позже.


После того, как мы получил объект StackWalker, у нас есть следующие варианты:


  • Выполнить на нём метод forEach() и пройтись по всем фреймам стека.
  • Вызвать метод getCallerClass() и получить класс, который вызвал наш метод (работает только с опцией RETAIN_CLASS_REFERENCE).
  • Вызвать метод walk(), который принимает функцию из Stream<StackFrame> в T, где T это что угодно. Это самый гибкий метод.

Для нашего логгера мы воспользуемся третьим вариантом. Вот как будет выглядеть реализация метода log():


public static void log(String message) {    String msg = StackWalker        .getInstance()        .walk((Stream<StackFrame> frames) -> {            StackFrame frame = frames.skip(2).findFirst().get();            return frame.getClassName() + "."                 + frame.getMethodName() + "("                 + frame.getFileName() + ":"                 + frame.getLineNumber() + ") "                 + message;        });    System.out.println(msg);}

Теперь давайте замерим производительность варианта со StackWalker:


@Benchmarkpublic String stackWalker() {    return StackWalker        .getInstance()        .walk(frames -> {            StackFrame frame = frames.skip(2).findFirst().get();            return frame.getClassName() + "."                 + frame.getMethodName() + "("                 + frame.getFileName() + ":"                 + frame.getLineNumber() + ")";        });}

Детали запуска бенчмарка
OpenJDK 64-Bit Server VM (build 16+36-2231, mixed mode, sharing)Intel Core i5-9500 3.00GHZОпции запуска: -f 1 -wi 3 -w 3 -i 5 -r 5 -t 6

Benchmark          Mode  Cnt    Score   Error  UnitsStack.stackTrace   avgt    5  103,704 ? 1,123  us/opStack.stackWalker  avgt    5    2,781 ? 0,156  us/op

Скорость выросла в 37 раз! Это огромный выигрыш. Конечно, 2.8 микросекунды это всё ещё далеко не бесплатно, но такой вариант кажется уже вполне приемлемым, чтобы включить его в боевом приложении.


Так как метод StackWalker.walk() даёт нам Stream, то наши возможности по обходу стека практически безграничны: можно получить все фреймы, можно отфильтровать, можно пропустить сколько-то. Например, усложним наш логирующий фреймворк, добавив туда уровни:


package org.mylogger;public final class MyLogger {    public enum Level {        ERROR, WARN, INFO    }    public static void error(String message) {        log(Level.ERROR, message);    }    public static void warn(String message) {        log(Level.WARN, message);    }    public static void info(String message) {        log(Level.INFO, message);    }    public static void log(Level level, String message) {        ...    }}

Сейчас мы уже не можем использовать конструкцию frames.skip(2), потому что метод log() может быть вызван как напрямую, так и через методы error(), warn(), log(), а значит фрейм надо искать немножко умнее. Самое простое через Stream.dropWhile():


public static void log(Level level, String message) {    String msg = StackWalker        .getInstance()        .walk((Stream<StackFrame> frames) -> {            StackFrame frame = frames                .dropWhile(f -> f.getClassName().startsWith("org.mylogger"))                .findFirst()                .get();            return level + " "                 + frame.getClassName() + "."                 + frame.getMethodName() + "("                 + frame.getFileName() + ":"                 + frame.getLineNumber() + ") "                 + message;        });    System.out.println(msg);}


Какие ещё применения есть у StackWalker?


Как вы знаете, в Java 9 появились модули. Но мало кто использует их в своих проектах, и подавляющее большинство всё ещё препочитает класть всё в classpath. Но тогда у нас теряется весьма ценная возможность экспортировать из модуля часть пакетов, а остальные скрывать. Представим, что у нас есть пакет org.example.mylib.internal, который мы хотим, чтобы не использовал никто кроме нашего модуля:


package org.example.mylib.internal;public final class Handler {    public static void handle() {        ...    }}

Класс Handler публичный, а значит компилятор уже никак не помешает кому угодно использовать этот класс. Но, может быть, можно сделать хотя бы проверку в рантайме? И действительно, это можно сделать через StackWalker.getCallerClass(), и выглядеть этот будет примерно так:


package org.example.mylib.internal;public final class Handler {    public static void handle() {        if (!StackWalker            .getInstance(Option.RETAIN_CLASS_REFERENCE)            .getCallerClass()            .getPackageName()            .startsWith("org.example.mylib.")) {            throw new RuntimeException("Security error");        }        ...    }}

Здесь мы использовали опцию RETAIN_CLASS_REFERENCE, иначе получить Class не получилось бы. В принципе, подобную функциональность можно было бы реализовать и через Stream.walk(), но getCallerClass() работает немного быстрее.



6. System.Logger


Появился в: Java 9


Если уж мы начали говорить про логирование, то нельзя не рассказать про новое стандартное API для логирования, которое появилось в Java 9. Это API очень маленькое и состоит всего из трёх классов: интерфейса System.Logger, абстрактного класса System.LoggerFinder и перечисления System.Logger.Level.


Использовать System.Logger чрезвычайно просто:


public final class Main {    private static final Logger LOGGER = System.getLogger("");    public static void main(String[] args)  {        LOGGER.log(Level.ERROR, "Critical error!");    }}

Вывод:


апр. 17, 2021 6:24:57 PM org.example.Main mainSEVERE: Critical error!

System.Logger это не новый очередной фреймворк для логирования, как вы могли бы подумать сначала, а только фронтенд для логирования. Если вы знакомы с существующими фреймворками, то вам должно быть это знакомо: например, SLF4J это фронтенд, а его соответствующим бэкендом является Logback. Или Log4j API это фронтенд для Log4j Core. Так вот, System.Logger это фронтенд для знакомого вам фреймворка java.util.logging, который находится в отдельном модуле java.logging.


При этом нам ничего не мешает для SLF4J использовать другой бэкенд, например, Log4j или java.util.logging. Или для Log4j API использовать SLF4J как бэкенд или java.util.logging. Точно так же и с System.Logger: он спроектирован так, чтобы реализация могла быть абсолютно любой. Если не нравится неудобный и старый java.util.logging, то можно использовать что-то другое. Например, можно его настроить на современный Log4j, для чего потребуется подключить следующие зависимости:


<dependency>    <groupId>org.apache.logging.log4j</groupId>    <artifactId>log4j-api</artifactId>    <version>2.14.1</version> <!-- Последняя версия на момент написания статьи -->    <scope>runtime</scope></dependency><dependency>    <groupId>org.apache.logging.log4j</groupId>    <artifactId>log4j-core</artifactId>    <version>2.14.1</version>    <scope>runtime</scope></dependency><dependency>    <groupId>org.apache.logging.log4j</groupId>    <artifactId>log4j-jpl</artifactId>    <version>2.14.1</version>    <scope>runtime</scope></dependency>

При этом саму программу изменять не потребуется: то, что log4j-jpl окажется в classpath, уже будет достаточно. Java через ServiceLoader найдёт нужную реализацию LoggerFinder в виде Log4jSystemLoggerFinder и будет направлять логи в Log4j:


18:24:57.941 [main] ERROR  - Critical error!

После этого модуль java.logging можно будет даже вообще исключить из JRE/JDK, ведь он больше не нужен (если вы конечно нигде не вызываете java.util.logging напрямую).


К сожалению, адаптера System.Logger для SLF4J/Logback нет. Но тут проблема более глобальная похоже, что сам проект SLF4J мёртв. Последний коммит в GitHub был полтора года назад. Так что Log4j сейчас это самый нормальный вариант он активно развивается и поддерживается.


Несколько примеров использования System.Logger:


LOGGER.log(Level.INFO, "Information");LOGGER.log(Level.DEBUG, "Sum of {} and {} is {}:", 2, 3, 2+3);LOGGER.log(Level.TRACE, () -> "Lazy message");LOGGER.log(Level.ERROR, "Log exception", new Exception());


7. Lookup.defineHiddenClass()


Появился в: Java 15


В прошлый раз мы рассказывали про метод MethodHandles.Lookup.defineClass(), с помощью которого можно во время выполнения легко загрузить класс в том же пакете, не создавая при этом новый загрузчик класса. Это очень удобный метод, но у него есть существенный минус: класс, который загружен таким образом, будет до конца жизни висеть в памяти (по крайней мере до конца жизни текущего загрузчика класса). Это не очень подходит для приложений, которым требуется динамически генерировать и загружать много временных классов на лету. Но с Java 15 появилась альтернатива в виде нового вида классов, которые называются скрытыми.


Скрытые классы создаются с помощью нового метода Lookup.defineHiddenClass(). По сути этот метод очень похож на старый нестандартный метод Unsafe.defineAnonymousClass(), который много лет используется различными фреймворками, поскольку решает проблему неконтролируемого роста количества временных классов в памяти. При этом сам Unsafe.defineAnonymousClass() с Java 15 стал deprecated for removal.


Скрытые классы имеют следующие особенности:


  • На них не могут прямо ссылаться другие классы. Всё их использование может осуществляться исключительно через рефлексию.
  • Они являются необнаружимыми. Их нельзя найти ни по имени, ни обнаружить с помощью загрузчиков классов (через Class.forName(), ClassLoader.loadClass(), ClassLoader.findLoadedClass() и т.д.). Однако скрытые классы не являются анонимными и всё-таки имеют имя в формате <имя класса в байт-коде>/<suffix> (например, org.example.Temp/0x0000000800cb8000).
  • Они реализованы таким способом, что не связаны сильной ссылкой с загрузчиком класса, а значит могут быть собраны сборщиком мусора, когда их Class становится недостижимым (но если хочется, то это можно переопределить, передав опцию ClassOption.STRONG в defineHiddenClass()).
  • Они не появляются в стектрейсах, если только не включить опции -XX:+UnlockDiagnosticVMOptions -XX:+ShowHiddenFrames.

Кстати, вы уже должны быть хорошо знакомы со скрытыми классами, поскольку используете их каждый день. Это лямбды:


jshell> Runnable runnable = () -> {}runnable ==> $Lambda$26/0x0000000800c0aa00@443b7951jshell> runnable.getClass().isHidden()$2 ==> true


Давайте создадим небольшой примерчик и определим свой скрытый класс с нуля. Пусть он для простоты складывает два int'а. Для разнообразия будем это делать не с помощью javac, а через ByteBuddy.


Для начала нужно создать представление класса в байт-коде в виде массива байтов:


byte[] bytes = new ByteBuddy()        .subclass(Object.class)        .name("org.example.Temp")        .defineMethod("sum", int.class, Modifier.PUBLIC)        .withParameters(int.class, int.class)        .intercept(new Implementation.Simple(                MethodVariableAccess.INTEGER.loadFrom(1),                MethodVariableAccess.INTEGER.loadFrom(2),                Addition.INTEGER,                MethodReturn.INTEGER))        .make()        .getBytes();

По сути мы скомпилировали вот такой класс, но сделали это в рантайме непосредственно через манипуляции с байт-кодом:


package org.example;public class Temp {    public int sum(int x, int y) {        return x + y;    }}

Теперь, когда у нас есть байт-код класса, можно его загружать и что-то с ним делать:


Lookup lookup = MethodHandles        .lookup()        .defineHiddenClass(bytes, false);// Для разнообразия будем использовать MethodHandle вместо reflectionObject obj = lookup        .findConstructor(lookup.lookupClass(), MethodType.methodType(void.class))        .invoke();MethodHandle sumHandle = lookup.findVirtual(lookup.lookupClass(), "sum",        MethodType.methodType(int.class, int.class, int.class));// Вызовем метод sum. Должен напечатать 5System.out.println(sumHandle.invoke(obj, 3, 2));

Вот и всё.



Кстати, так как скрытые классы необнаружимы, то загружать один и тот же класс можно сколько угодно раз. По сути каждый раз будет определяться новый уникальный скрытый класс:


Lookup lookup1 = MethodHandles.lookup().defineHiddenClass(bytes, false);Lookup lookup2 = MethodHandles.lookup().defineHiddenClass(bytes, false);Lookup lookup3 = MethodHandles.lookup().defineHiddenClass(bytes, false);System.out.println(lookup1.lookupClass()); // class org.example.Temp/0x0000000800cb4000System.out.println(lookup2.lookupClass()); // class org.example.Temp/0x0000000800cb4400System.out.println(lookup3.lookupClass()); // class org.example.Temp/0x0000000800cb4800


8. Новые методы в Math


Появились в: Java 9 / Java 15


Наверное, практически все, кто начинал работать с большими числами в Java, совершал вот такую ошибку:


int x = ...int y = ...long z = x * y;

Это один из тех примеров в Java, когда можно угодить в ловушку даже на простом умножении: произведение двух int это тоже int, а значит то, что переменная z имеет тип long, от переполнения никак не спасает. Для исправления этой ошибки нужно явно прикастовать хотя бы один из множителей к long:


long z = (long) x * y;

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


Возьми два int и перемножь их в long с учётом переполнения

И с Java 9 такой способ есть. Это метод Math.multiplyFull():


long z = Math.multiplyFull(x, y);


Вообще обработка переполнений в Java реализуется довольно муторно, и чтобы облегчить жизнь программистам, в Java 8 появилась целая пачка методов для этого в классе Math:


  • int toIntExact(long value)
  • int incrementExact(int a)
  • long incrementExact(long a)
  • int decrementExact(int a)
  • long decrementExact(long a)
  • int negateExact(int a)
  • long negateExact(long a)
  • int addExact(int x, int y)
  • long addExact(long x, long y)
  • int subtractExact(int x, int y)
  • long subtractExact(long x, long y)
  • int multiplyExact(int x, int y)
  • long multiplyExact(long x, long y)

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


jshell> Math.abs(Integer.MIN_VALUE)$1 ==> -2147483648

Как же так, ведь модуль это положительное число? Это так, но дело в том, что 2147483648 просто не влезает в int, поэтому этот отдельный случай является исключением. Чтобы вот так по чистой случайности не напороться на такое, можно обезопасить себя методом Math.absExact(), который появился в Java 15:


jshell> Math.absExact(Integer.MIN_VALUE)|  Exception java.lang.ArithmeticException: Overflow to represent absolute value of Integer.MIN_VALUE|        at Math.absExact (Math.java:1392)|        at (#1:1)jshell> Math.absExact(Long.MIN_VALUE)|  Exception java.lang.ArithmeticException: Overflow to represent absolute value of Long.MIN_VALUE|        at Math.absExact (Math.java:1438)|        at (#2:1)


А знаете ли вы, сколько будет, если найти целое от деления -11 на 3? А остаток? Давайте проверим:


jshell> -11 / 3$1 ==> -3jshell> -11 % 3$2 ==> -2

Ну вроде бы логично, ведь -11 = 3 * (-3) - 2. Однако если вы выполните то же самое, например, на Python, то получите совсем иной результат:


>>> -11 / 3-4>>> -11 % 31

И этот результат тоже по-своему верный: -11 = 3 * (-4) + 1. Дело в том, что есть два способа деления целых чисел: с обрезанием в сторону нуля и с обрезанием в сторону минус бесконечности. Java выбрала первый способ, Python второй. Ну а что делать, если я хочу в Java делить по-питоновски? Для этого в Java 9 появились методы Math.floorDiv() и Math.floorMod():


jshell> Math.floorDiv(-11, 3)$1 ==> -4jshell> Math.floorMod(-11, 3)$2 ==> 1


Также для совсем упоротых математиков в Java 9 появились два метода Math.fma(float, float, float) и Math.fma(double, double, double), которые делают то же самое, что и a * b + c, но только точнее, потому что используют специальную отдельную инструкцию процессора:


jshell> Math.fma(2.99, 5.91, 7.1)$1 ==> 24.7709jshell> 2.99 * 5.91 + 7.1$2 ==> 24.770900000000005


9. Аннотация java.io.Serial


Появилась в: Java 14


Используете ли вы стандартную сериализацию в Java? Этот механизм далеко не идеальный и со своими недостатками, но иногда он может быть очень удобным, потому что позволяет из коробки очень просто сделать сереализацию и десериализацию Java-объектов. Рассмотрим пример:


public class Point {    private static final long serialVersionUID = 1L;    public int x;    public int y;}

Чтобы сконвертировать объект Point в массив байтов, нужно написать всего несколько строчек:


var point = new Point();point.x = 1;point.y = 2;var baos = new ByteArrayOutputStream();try (var oos = new ObjectOutputStream(baos)) {    oos.writeObject(point);}byte[] bytes = baos.toByteArray();


Очень удобно. Кстати, вы заметили ошибку в моём коде? Конечно же, я забыл реализовать интерфейс Serializable! (Я специально добавил поле serialVersionUID, чтобы отвлечь ваше внимание.)


Правильный код будет таким:


public class Point implements Serializable {    private static final long serialVersionUID = 1;    public int x;    public int y;}

И вот это как раз и есть одна из главных проблем сериализации при её использовании можно очень легко допустить ошибку: забыть Serializable, опечататься в названии поля serialVersionUID, забыть сделать его статическим и т.д. Чтобы немного обезопаситься от такого, в Java 14 ввели новую аннотацию Serial.


Этой аннотацией теперь рекомендуется помечать все поля и методы, относящиеся к механизму сериализации:


public class Point implements Serializable {    @Serial    private static final long serialVersionUID = 1;    ...}

Теперь, если будет допущена ошибка, то появится предупреждение:


public class Point {    @Serial // Annotated member is not a part of the serialization mechanism    private static final long serialVersionUID = 1;    ...}

Или:


public class Point implements Serializable {    @Serial // Annotated member is not a part of the serialization mechanism    private static final int serialVersionUID = 1;    ...}

Аннотация будет делать проверки на всех полях и методах, которые относятся к сериализации: serialVersionUID, serialPersistentFields, writeObject(), readObject() и т.д.


К сожалению, на текущий момент предупреждения хорошо работают только в IntelliJ IDEA. В компиляторе JDK 16 проверки выполняются только с включённым флагом -Xlint:serial и работают не все. Например, для двух примеров выше javac ругается только во втором случае:


> javac -Xlint:serial Point.javaPoint.java:6: warning: [serial] serialVersionUID must be of type long in class Point    private static final int serialVersionUID = 1;                             ^

Возможно, это исправят в Java 17.



10. Методы Objects: checkIndex(), checkFromIndexSize(), checkFromToIndex()


Появились в: Java 9 / Java 16


Завершим нашу статью несколькими полезными методами для проверки индексов.


Иногда приходится писать функции, принимающие в качестве входных параметров индексы или диапазоны индексов, и чтобы начать использовать эти индексы, нужно сначала убедиться, что они не выходят за границы. То есть приходится писать подобные проверки в начале методов:


private static void getAt(int index, int length) {    if (index < 0) {        throw new IllegalArgumentException("index < 0");    }    if (index >= length) {        throw new IllegalArgumentException("index >= length");    }    ...}

Если подобных функций в проекте становится уже несколько, то чтобы не повторяться, такие проверки удобнее вынести в отдельные утилитные методы:


public final class PreconditionUtils {    public static void checkIndex(int index, int length) {        if (index < 0) {            throw new IllegalArgumentException("index < 0");        }        if (index >= length) {            throw new IllegalArgumentException("index >= length");        }    }}


Но с Java 9 теперь это больше не нужно, потому что в классе Objects теперь есть стандартные методы проверок индексов.


Метод Objects.checkIndex() проверяет, что индекс находится в диапазоне [0, length):


jshell> Objects.checkIndex(-3, 10)|  Exception java.lang.IndexOutOfBoundsException: Index -3 out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckIndex (Preconditions.java:70)|        at Preconditions.checkIndex (Preconditions.java:248)|        at Objects.checkIndex (Objects.java:372)|        at (#1:1)jshell> Objects.checkIndex(10, 10)|  Exception java.lang.IndexOutOfBoundsException: Index 10 out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckIndex (Preconditions.java:70)|        at Preconditions.checkIndex (Preconditions.java:248)|        at Objects.checkIndex (Objects.java:372)|        at (#2:1)


Метод Objects.checkFromIndexSize() проверяет, что диапазон [fromIndex, fromIndex + size) находится в диапазоне [0, length):


jshell> Objects.checkFromIndexSize(3, 8, 10)|  Exception java.lang.IndexOutOfBoundsException: Range [3, 3 + 8) out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckFromIndexSize (Preconditions.java:82)|        at Preconditions.checkFromIndexSize (Preconditions.java:343)|        at Objects.checkFromIndexSize (Objects.java:424)|        at (#3:1)jshell> Objects.checkFromIndexSize(-2, 8, 10)|  Exception java.lang.IndexOutOfBoundsException: Range [-2, -2 + 8) out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckFromIndexSize (Preconditions.java:82)|        at Preconditions.checkFromIndexSize (Preconditions.java:343)|        at Objects.checkFromIndexSize (Objects.java:424)|        at (#4:1)jshell> Objects.checkFromIndexSize(3, -4, 10)|  Exception java.lang.IndexOutOfBoundsException: Range [3, 3 + -4) out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckFromIndexSize (Preconditions.java:82)|        at Preconditions.checkFromIndexSize (Preconditions.java:343)|        at Objects.checkFromIndexSize (Objects.java:424)|        at (#5:1)


Наконец, метод Objects.checkFromToIndex() проверяет, что диапазон [fromIndex, toIndex) находится в диапазоне [0, length):


jshell> Objects.checkFromToIndex(3, 11, 10)|  Exception java.lang.IndexOutOfBoundsException: Range [3, 11) out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckFromToIndex (Preconditions.java:76)|        at Preconditions.checkFromToIndex (Preconditions.java:295)|        at Objects.checkFromToIndex (Objects.java:398)|        at (#6:1)jshell> Objects.checkFromToIndex(-4, 8, 10)|  Exception java.lang.IndexOutOfBoundsException: Range [-4, 8) out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckFromToIndex (Preconditions.java:76)|        at Preconditions.checkFromToIndex (Preconditions.java:295)|        at Objects.checkFromToIndex (Objects.java:398)|        at (#7:1)jshell> Objects.checkFromToIndex(6, 4, 10)|  Exception java.lang.IndexOutOfBoundsException: Range [6, 4) out of bounds for length 10|        at Preconditions.outOfBounds (Preconditions.java:64)|        at Preconditions.outOfBoundsCheckFromToIndex (Preconditions.java:76)|        at Preconditions.checkFromToIndex (Preconditions.java:295)|        at Objects.checkFromToIndex (Objects.java:398)|        at (#8:1)


Кроме того, в Java 16 появились перегрузки этих функций для long:




Заключение


Сегодня я рассказал про 10 интересных API, некоторые из которых появились в буквально только что вышедшей Java 16, а некоторые уже присутствуют довольно давно ещё с 9-й версии. Надеюсь, что после прочтения данной статьи вы стали более заинтересованными в миграции на последнюю версию Java. Помните, что в новых версиях Java появляются не только новые возможности, но и изменения, ломающие обратную совместимость (1, 2, 3, 4, 5, 6, 7, 8). И чем больше вы тянете с переходом с Java 8 на последнюю версию, тем сложнее вам будет осуществить этот переход.


Продолжение следует...

Подробнее..
Категории: Java , Logging , Jmh , Java15 , Java14 , Stackwalker , Java16 , Java11 , Java9 , Java8 , String , Log4j , Slf4j , Stream , Serializable

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru