Книга Скотта Оукса «Эффективный Java»
Всем привет! Я продолжаю прокачивать свои знания про Java, и чем больше я читаю про внутреннее устройство его виртуальной машины (JVM), тем больше проникаюсь уважением к этой технологии. Сегодня хочу рассказать про книжку Скотта Оукса «Эффективный Java. Тюнинг кода на Java 8, 11 и дальше», которую дочитал на днях. Это уже второе издание книги, которая на русском языке вышла в 2021 году, а английская версия была написана в 2020 году, поэтому несмотря на то, что на обложке упоминается лишь Java 11 (с не очень убедительной припиской «и дальше»), книга достаточно актуальна. Просто на момент ее издания, долгоподдерживаемой версией (LTS) была Java 11, вышедшая в 2018 году.
Это очень подробная книга о том, что происходит «под капотом» среды выполнения Java, и как это можно использовать для написания более продуктивного кода. Или даже скорее о том, каких ситуаций лучше избегать. Это не учебник про язык Java, и кода там не так уж и много. В основном код здесь — это небольшие тестовые примеры, по которым измеряется производительность виртуальной машины в различных ситуациях и с различными настройками. Поскольку сейчас Java во многих случаях используется для написания серверных приложений, то и примеры часто делают упор на такое применение. Ничего про Java на Android в этой книге не сказано — там своя виртуальная машина Java, для которой надо писать свою отдельную книгу. В этой книге речь идет про виртуальную машину от Oracle и OpenJDK. Также надо сказать, что эта книга не про байт-код, который создается компилятором Java (хотя про него изредка речь заходила), а про то, как виртуальная машина и сборщики мусора работают в процессе выполнения приложения.
Коротко пробегусь по основным темам, которые затрагиваются в этой книге.
Все начинается с того, как писать тесты для оценки производительности (бенчмарки), как их можно разделить по уровням, на которых оценивается производительность. Здесь для себя нужно ответить на вопрос: мы хотим измерить время работы какого-то небольшого участка кода или целого приложения при сложном взаимодействии его частей? Как написать тесты, чтобы они имели практический смысл, то есть чтобы реальное приложение использовало часть кода так же, как и тестовое приложение. Как учесть «время разогрева» виртуальной машины, когда код еще находится в процессе оптимизации, что особенно важно, если вы хотите тестировать производительность серверов, которые уже долго работают. Или наоборот, вы хотите ускорить время запуска приложения?
На протяжении всей книги рассказывается о различных флагах настроек виртуальной машины, которые влияют на способ компиляции байт-кода, алгоритмы выделения и очистки памяти, работу уборщика мусора и т.д. Много внимания уделено многопоточной работе и синхронизации потоков. Часто говорится о различиях в поведении виртуальных машин разных версий (в основном сравниваются Java 8 и Java 11).
В этой книге также подробно рассказывается об инструментах, которые используются для тестирования кода и его профилирования. Например, для написания бенчмарков используется фреймворк JMH, входящий в OpenJDK. Отдельная глава посвящена инструментам, которые позволяют в реальном времени следить за работой приложения (профилировщики). Это, конечно, не документация по инструментам, но она дает общее представление об инструментах, и какие есть полезные параметры командной строки. Например, в книге неплохо описана утилита jcmd, которая позволяет подключиться к уже запущенной виртуальной машине и получить некоторую информацию о процессе работы в реальном времени. Для некоторых типов мониторинга используются не инструменты из состава JDK, а инструменты операционной системы Linux или Windows.
Подробно рассказывается о принципах действия профайлеров, о том, что они бывают разных типов. В этой главе упоминаются такие инструменты как Oracle Developer Studio, Java VisualVM, Java Flight Recorder, (JFR) Java Mission Control (JMC).
Одна глава книги посвящена работе JIT-компилятора. В ней рассказывается о том, что происходит с байт-кодом с момента запуска приложения и при дальнейшей работе, то, как JIT-компилятор выбирает, что нужно оптимизировать в первую очередь, как это влияет на используемую память и как с помощью параметров командной строки при запуске приложения можно влиять на работу JIT-компилятора. В этой главе коротко упоминается о виртуальной машине GraalVM.
Пожалуй, самая объемная часть книги — это главы, посвященные уборщикам мусора и принципам работы Java с памятью. Поскольку именно уборка мусора особенно сильно влияет на общую производительность приложений, то эта тема красной нитью проходит через большую часть глав книги. Многие параметры запуска виртуальной машины направлены на то, чтобы сделать уборку мусора как можно более безболезненной операцией с учетом особенностей выделения памяти в конкретном приложении (например, память выделяется часто, но небольшими порциями или редко, но сразу большими участками?) В книге рассказывается о разных сборщиках мусора, которые существуют в Java. Некоторые из них уже считаются устаревшими, но они могут быть полезны при запуске приложения на старых одноядерных компьютерах, другие на момент написания книги (в тот момент уже вышла Java 12) еще находились в экспериментальной стадии разработки.
В книге рассказывается о последовательной и параллельной уборке мусора, особо подробно рассказывается о уборщике мусора G1, поскольку уже на протяжении многих версий он стал использоваться по умолчанию, коротко сказано об устаревшем алгоритме CMS и о новых уборщиках мусора ZGC, Shenandoah GC и Epsilon GC. На момент написания книги последние три еще не рекомендовались для использования в проде, но вот сейчас я посмотрел информацию о них: ZGC уже Production Ready, начиная с JDK 15; Shenandoah GC вроде тоже довели до ума, но этот уборщик мусора не поддерживает Oracle, его поддержка есть только в OpenJDK; Epsilon GC — это уборщик мусора, который не убирает мусор, вроде бы он до сих пор остается в режиме экспериментальной поддержки.
В книге приводится большое количество бенчмарков использования этих уборщиков мусора в различных ситуациях и различными настройками. На их эффективность очень влияет не только тип приложений и его работа с памятью, но и железо, на котором запускается виртуальная машина: количество ядер процессора и количество оперативной памяти, которая доступна для виртуальной машины. Это особенно актуально для Docker-контейнеров, где можно ограничивать аппетиты контейнеров.
Со сборщиками мусора непосредственно связана тема выделения памяти приложением. Здесь говорится о том, как можно писать приложение, чтобы уборка мусора происходила наименее заметно, и можно ли обойтись без «остановки мира» на полную уборку мусора. Автор рассматривает разные подходы работы с памятью, говорит про создание пула объектов, и почему это не всегда хорошая идея. Речь заходит о мягких и слабых ссылках, в чем между ними разница, когда их имеет смысл использовать, а когда это создает лишнюю нагрузку на уборщика мусора. Тут же рассказывается о том, как анализировать срезы памяти в конкретные моменты времени, какие инструменты существуют, и что из такого подхода можно узнать полезного. Говорится о том, какие есть способы экономии памяти, и чем при этом приходится расплачиваться.
Также достаточно много внимания уделено многопоточному программированию, блокировкам потоков и как многопоточность влияет на сборку мусора, какие могут возникать не очевидные проблемы, например, если JIT-компилятор решит кэшировать некоторые переменные.
От многопоточности автор плавно переходит к теме блокировкам потока при вводе/выводе и асинхронному программированию. Он подробно рассматривает, как в этом случае влияет количество созданных потоков выполнения, как оценить, сколько таких потоков нужно создавать. А от ввода/вывода повествование логично переходит к эффективности работы с базами данных. В этой главе в основном говорится о блокировках при транзакциях и о способах блокировки данных в таблицах баз данных при запросах. Коротко рассказывается о разных типах драйверов для JDBC.
И, наконец, последняя глава посвящена различным особенностям JDK, которые могут повлиять на производительность. Это очень разнородная глава, куда вошли такие темы как работа со строками, действительно ли стоит избегать конкатенации строк (спойлер: далеко не всегда), как можно сэкономить память, которая тратится на хранение строк. Здесь же рассказывается про буферизированный ввод/ вывод, загрузку классов при запуске приложения, есть ли смысл использовать низкоуровневый интерфейс Java Native Interface (JNI), и когда его использование только ухудшит производительность (спойлер: почти всегда), насколько исключения влияют на производительность (спойлер: как-то влияют, но не настолько, чтобы об этом задумываться в большинстве случаев), как эффективно вести логи, есть ли разница между анонимными классами и лямбда-выражениями (спойлера не будет), чем хороши потоки данных (streams) и на что обратить внимание при сериализации объектов.
Когда я покупал эту книжку, то я не ожидал, что в таком относительно небольшом объеме (чуть меньше 500 страниц) будет такая концентрация полезных знаний. Воды в книге практически нет, но читается она несколько тяжеловато. Правда, я не знаю, виноват в этом автор или переводчики. Я периодически зависал над фразами, пытаясь распутать сложносочиненные предложения и понять, какой эпитет к какому описываемому объекту относится, причем не всегда это удавалось.
Если вы программируете на Java, то книжку однозначно стоит прочитать. До нее у меня были некоторые предвзятости относительно производительности виртуальной машины Java, но оказывается, это было верно для старых версий, а в последнее время ситуация значительно улучшилась, и вообще имеет смысл всегда переходить на более новые версии JDK.
PS. Вы можете подписаться на новости сайта через RSS, Группу Вконтакте или Канал в Telegram.
Alaric:
По поводу производительности JVM. Я подписан на несколько рассылок по Java и за этот год я неоднократно видел рекомендации в духе, мол, если вы хотите ускорить своё приложение, то просто обновитесь на новую версию (в этом году вышел новый LTS-релиз — Java 17) и это само по себе даст вам прирост производительности.
> есть ли разница между анонимными классами и лямбда-выражениями
Кажется, в одной из недавних рассылок я видел упоминание, что сейчас идут обсуждения о том, чтобы это поменять, но при поиске я не смог найти ссылку, поэтому, возможно, я что-то путаю.
9 января 2022, 5:20 ппJenyay:
Да, автор тоже часто пишет, что часто обновление JDK и перекомпиляция под него дает заметный прирост производительности. Но он в основном пишет про переход с Java 8 на Java 11.
В том проекте, где я участвую, мы уже перешли на Java 17, правда про производительность ничего не скажу, потому что проект только недавно начался, и на Java 11 производительность не измеряли. Но языковые плюшки вроде многострочных строк и records мне нравятся.
9 января 2022, 8:45 пп