Эффективный Java. Тюнинг кода на Java 8, 11 и дальше. 2-е межд. издание - Скотт Оукс - E-Book

Эффективный Java. Тюнинг кода на Java 8, 11 и дальше. 2-е межд. издание E-Book

Скотт Оукс

0,0
10,49 €

-100%
Sammeln Sie Punkte in unserem Gutscheinprogramm und kaufen Sie E-Books und Hörbücher mit bis zu 100% Rabatt.
Mehr erfahren.
Beschreibung

Программирование и тестирование обычно принято относить к разным профессиональным сферам. Скотт Оукс — признанный эксперт по языку Java — уверен, что если вы хотите работать с этим языком, то обязаны понимать, как выполняется код в виртуальной машине Java, и знать, какие настройки влияют на производительность. Вы сможете разобраться в производительности приложений Java в контексте как JVM, так и платформы Java, освоите средства, функции и процессы, которые могут повысить производительность в LTS-версиях Java, и познакомитесь с новыми возможностями (такими как предварительная компиляция и экспериментальные сборщики мусора). В этой книге вы: - Узнаете, как платформы и компиляторы Java влияют на производительность. - Разберетесь c механизмом сборки мусора. - Освоите четыре принципа получения наилучших результатов при тестировании производительности. - Научитесь пользоваться JDK и другими инструментами оценки производительности. - Узнаете как настройка и приемы программирования позволяют минимизировать последствия сборки мусора. - Научитесь решать проблемы производительности средствами Java API. - Поймете, как улучшить производительность приложений баз данных Java.

Das E-Book können Sie in Legimi-Apps oder einer beliebigen App lesen, die das folgende Format unterstützen:

EPUB
MOBI

Seitenzahl: 666

Veröffentlichungsjahr: 2023

Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Скотт Оукс
Эффективный Java. Тюнинг кода на Java 8, 11 и дальше. 2-е межд. издание
2021

Переводчики Е. Матвеев, И. Сигайлюк

Литературный редактор М. Петруненко

Художник В. Мостипан

Корректоры С. Беляева, М. Молчанова (Котова)

Верстка Л. Егорова

Скотт Оукс

Эффективный Java. Тюнинг кода на Java 8, 11 и дальше. 2-е межд. издание. — СПб.: Питер, 2021.

ISBN 978-5-4461-1757-4

© ООО Издательство "Питер", 2021

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

Оглавление

Предисловие
Для кого написана эта книга
Благодарности
От издательства
Глава 1. Введение
Структура книги
Платформы и соглашения
Аппаратные платформы
Производительность: общая картина
Итоги
Глава 2. Тестирование производительности
Тестирование реального приложения
Пропускная способность, пакетирование и время отклика
Примеры хронометражных тестов
Итоги
Глава 3. Инструментарий производительности Java
Средства операционной системы и анализ
Средства мониторинга Java
Средства профилирования
Инструментальные профилировщики
Java Flight Recorder
Итоги
Глава 4. Работа с JIT-компилятором
JIT-компиляторы: общие сведения
Многоуровневая компиляция
Распространенные флаги компилятора
Флаги компилятора высокого уровня
Потоки компиляции
Плюсы и минусы многоуровневой компиляции
GraalVM
Предварительная компиляция
Итоги
Глава 5. Знакомство с уборкой мусора
Общие сведения об уборке мусора
Основная настройка уборщика мусора
Инструменты уборки мусора
Включение протоколирования уборки мусора в JDK 8
Включение протоколирования уборки мусора в JDK 11
Итоги
Глава 6. Алгоритмы уборки мусора
Параллельный уборщик мусора
Уборщик мусора G1
Уборщик мусора CMS
Расширенная настройка
Экспериментальные алгоритмы уборки мусора
Итоги
Глава 7. Практика работы с памятью кучи
Анализ кучи
Снижение потребления памяти
Управление жизненным циклом объекта
Итоги
Глава 8. Практика работы с низкоуровневой памятью
Потребление памяти
Настройки JVM для операционной системы
Итоги
Глава 9. Производительность многопоточных программ и синхронизации
Многопоточное выполнение и оборудование
Пулы потоков и объекты ThreadPoolExecutor
ForkJoinPool
Синхронизация потоков
Настройки потоков JVM
Мониторинг потоков и блокировок
Итоги
Глава 10. Серверы Java
Java NIO
Серверные контейнеры
Асинхронные исходящие вызовы
Обработка JSON
Итоги
Глава 11. Практика работы с базами данных
База данных для примера
JDBC
JPA
Spring Data
Итоги
Глава 12. Рекомендации по использованию Java SE API
Строки
Буферизованный ввод/вывод
Загрузка классов
Случайные числа
Интерфейс JNI
Исключения
Журналы
API коллекций Java
Лямбда-выражения и анонимные классы
Производительность потоков данных и фильтров
Сериализация объектов
Итоги
Приложение. Список флагов настройки

Предисловие

Когда издательство O’Reilly впервые обратилось ко мне с предложением написать книгу о настройке производительности Java, я засомневался. «Производительность Java, — подумал я. — Разве эта тема еще не исчерпана?» Да, я работаю над улучшением производительности приложений Java (и других приложений) в своей повседневной работе, но я предпочитаю думать, что трачу большую часть своего времени на борьбу с алгоритмическими неэффективностями и узкими местами внешних систем, а не на причины, связанные напрямую с оптимизацией Java.

После некоторых размышлений я понял, что в этом случае (как обычно) обманывал себя. Безусловно, производительность комплексных систем отнимает у меня немало времени, и я иногда сталкиваюсь с кодом, который использует алгоритм O (n2), хотя мог бы использовать алгоритм со сложностью O(log N). И мне ежедневно приходится думать об эффективности уборки мусора, или производительности компилятора JVM, или о том, как добиться наилучшей производительности различных Java API.

Я вовсе не собираюсь принижать колоссальный прогресс в области производительности Java и JVM за последние двадцать с лишним лет. Когда я работал в отделе Java-евангелизма в компании Sun в конце 1990-х годов, единственным реальным «эталонным тестом» была программа CaffeineMark 2.0 от Pendragon software. По разным причинам структура этой программы ограничивала ее ценность; и все же в те дни мы обожали говорить всем, что на основании этого теста производительность Java 1.1.8 в 8 раз превышает производительность Java 1.0. И надо сказать, это было чистой правдой — у Java 1.1.8 появился JIT-компилятор, тогда как версия Java 1.0 была практически полностью интерпретируемой.

Затем комитеты по стандартизации начали разрабатывать более строгие тесты, и оценка производительности Java стала строиться вокруг этих тестов. Результатом стало непрерывное улучшение всех областей JVM — уборки мусора, компиляции и API. Конечно, этот процесс продолжается и сегодня, но как ни странно, работа в области производительности становится все сложнее и сложнее. Восьмикратное повышение производительности за счет введения JIT-компилятора было достаточно прямолинейным технологическим достижением; и хотя компилятор продолжает совершенствоваться, таких улучшений мы уже не увидим. Параллелизация уборщика мусора обеспечила огромный прирост производительности, но последние изменения были не столь значительными.

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

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

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

За несколько последних лет проблемы производительности разделились на два направления. С одной стороны, очень большие машины, на которых могут выполняться JVM с очень большим размером кучи, стали рядовым явлением. Для новых условий в JVM появился новый уборщик мусора (G1), который — как новая технология — требует чуть большей ручной настройки, чем традиционные уборщики мусора. В то же время облачные технологии возродили актуальность небольших однопроцессорных машин: вы можете воспользоваться услугами Oracle, Amazon или другой компании и недорого арендовать однопроцессорную машину для работы небольшого сервера приложения. (На самом деле вы получаете не однопроцессорную машину, а виртуальный образ ОС на очень большой машине, но виртуальная ОС ограничена использованием одного процессора. С точки зрения Java это то же самое, что однопроцессорная машина.) В таких средах правильное управление небольшими объемами памяти начинает играть важную роль.

Платформа Java также продолжает развиваться. Каждое новое издание Java предоставляет новые языковые средства и новые API, повышающие эффективность труда разработчиков (даже если они не всегда повышают эффективность приложений). От эффективного применения этих языковых средств может зависеть судьба приложения — провал или коммерческий успех. А эволюция платформы поднимает ряд интересных вопросов из области производительности: бесспорно, использование JSON для обмена информацией между двумя программами намного проще оптимизированного закрытого протокола. Экономия времени разработчика важна, но вы должны следить за тем, чтобы эффективность разработки сопровождалась приростом производительности.

Для кого написана эта книга

Эта книга написана для специалистов по производительности и разработчиков, которые желают разобраться в различных аспектах влияния JVM и Java API на производительность. Если сейчас воскресный вечер, в понедельник ваш сайт должен принять первых посетителей, а вы хотите на скорую руку решить проблемы с производительностью — эта книга не для вас.

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

Однако на фундаментальном уровне, для того чтобы ваши программы Java работали действительно быстро, необходимо глубоко понимать, как работает JVM (и Java API). Существуют сотни флагов настройки Java, и настройка JVM не должна сводиться к тому, чтобы бездумно изменять эти флаги и смотреть, что из этого получится. Вместо этого я постарался подробно рассказать о том, что делают JVM и API, в надежде, что вы поймете суть происходящего, разберетесь в конкретном поведении приложения и осознаете, почему оно неэффективно работает. После этого вам останется решить простую (или относительно простую) задачу: избавиться от нежелательного (неэффективного) поведения.

У работы в области производительности Java есть один интересный аспект: по уровню подготовки разработчики часто сильно отличаются от инженеров из группы производительности или контроля качества. Я знаю разработчиков, которые помнят тысячи сигнатур малоизвестных методов, редко используемых в Java API, но понятия не имеют, что делает флаг -Xmn. И я знаю инженеров по тестированию, которые могут выжать последнюю каплю производительности установкой различных флагов уборщика мусора, но едва смогут написать программу «Hello World» на Java.

Производительность Java включает обе области: настройку флагов компилятора, уборщика мусора и т.д., а также оптимальное использование API. Я буду считать, что вы хорошо умеете писать программы на Java. Даже если вас в первую очередь интересуют непрограммные аспекты Java, мы проведем немало времени за обсуждением программ, включая примеры, которые предоставляют тестовые данные для примеров.

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

Благодарности

Хочу поблагодарить всех, кто помогал мне в работе над книгой. Во многих отношениях в ней были собраны знания, накопленные мной за последние 20 лет в Java Performance Group и других технических группах в Sun Microsystems и Oracle, так что список людей, поделившихся своим мнением о книге, весьма обширен. Всем инженерам, с которыми я работал за это время, и особенно тем, которые терпеливо отвечали на мои вопросы в течение последнего года, — спасибо!

Хочу особо поблагодарить Стэнли Гуана (Stanley Guan), Азима Дживу (Azeem Jiva), Ким ЛиЧонга (Kim LiChong), Дипа Сингха (Deep Singh), Мартина Вербурга (Martijn Verburg) и Эдварда Юэ Шун Вона (Edward Yue Shung Wong) за время, потраченное на рецензирование черновиков и предоставление исключительно ценной обратной связи. Уверен, что им не удалось найти все мои ошибки, хотя материал книги сильно улучшился на основании полученной от них информации. Второе издание было серьезно улучшено благодаря всеобъемлющей и вдумчивой помощи, которую предоставили Бен Эванс (Ben Evans), Род Хилтон (Rod Hilton) и Майкл Хангер (Michael Hunger). Мои коллеги Эрик Каспоул (Eric Caspole), Чарли Хант (Charlie Hunt) и Роберт Страут (Robert Strout) из группы производительности Oracle HotSpot также терпеливо помогали мне разобраться с различными проблемами второго издания.

Сотрудники издательства O’Reilly были как всегда отзывчивы и готовы прийти на помощь. Мне посчастливилось работать с редактором Мэг Бланшетт (Meg Blanchette) над первым изданием, а Амелия Блевинс (Amelia Blevins) тщательно и усердно руководила работой над вторым изданием. Спасибо за всю помощь и содействие!

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

От издательства

Ваши замечания, предложения, вопросы отправляйте по адресу [email protected] (издательство «Питер», компьютерная редакция).

Мы будем рады узнать ваше мнение! На веб-сайте издательства www.piter.com вы найдете подробную информа­цию о наших книгах.

Глава 1. Введение

Эта книга посвящена науке и искусству настройки производительности Java.

«Научная» часть этого утверждения никого не удивит; любые обсуждения, относящиеся к производительности, включают множество цифр, метрик и аналитики. Многие специалисты по оптимизации имеют хорошую теоретическую подготовку, а применение научной строгости является важнейшим аспектом обеспечения максимального быстродействия.

Но как насчет «искусства»? Представление о том, что оптимизация сочетает в себе науку и искусство, вряд ли можно назвать новым, но обычно оно не находит явного подтверждения в обсуждениях по поводу производительности. Отчасти это объясняется тем, что концепция «искусства» не является тем, чему нас учили. Однако то, что одним кажется искусством, по большому счету основано на глубоких знаниях и опыте. Говорят, что любая достаточно развитая технология неотличима от магии1; и бесспорно, рыцарю Круглого стола мобильный телефон покажется чем-то волшебным. Аналогичным образом результат, которого может добиться хороший специалист по оптимизации, может показаться произведением искусства, однако это искусство на самом деле является результатом применения глубоких знаний, опыта и интуиции.

Что касается опыта и интуиции, книга вам помочь не сможет. Зато она может предоставить глубокие знания — в той перспективе, что систематическое применение знаний со временем способно развить навыки, необходимые для того, чтобы стать хорошим специалистом по производительности Java. Она написана для того, чтобы дать читателю глубокое понимание различных аспектов производительности платформы Java.

Эти знания делятся на две широкие категории. К первой относится производительность самой виртуальной машины Java (JVM, Java Virtual Machine): конфигурация JVM влияет на многие аспекты производительности программы. Разработчиков с опытом работы на других языках необходимость настройки может раздражать, хотя на самом деле процесс настройки JVM полностью аналогичен выбору флагов компилятора в процессе компиляции для программистов C++, настройке соответствующих переменных в файле php.ini для PHP-разработчиков и т.д.

Второй аспект, в котором вы должны хорошо разбираться, — влияние функциональности платформы Java на производительность. Обратите внимание на термин «платформа»: некоторые возможности (например, многопоточность и синхронизация) являются частью языка, тогда как другие (например, опе­рации со строками) относятся к стандартному Java API. Хотя между языком Java и Java API существуют важные различия, в данном контексте они будут рассматриваться на одном уровне. В книге рассматриваются обе грани платформы.

Производительность JVM зависит прежде всего от флагов оптимизации, тогда как производительность платформы в большей степени определяется применением передовых практик в коде приложения. Долгое время они считались разными специализациями: разработчики программируют, а группа производительности проводит тестирование и рекомендует исправления для выявленных проблем с производительностью. Это различие никогда не приносило особой пользы — каждый программист, работающий на Java, должен в равной степени понимать, как код ведет себя в JVM и какие виды оптимизации с большой вероятностью улучшат его производительность. С переходом проектов на модель DevOps эти различия постепенно становятся менее жесткими. Знание сферы в общем придаст вашим работам налет искусства.

Структура книги

Обо всем по порядку: в главе 2 рассматриваются общие методологии тестирования приложений Java, включая некоторые ловушки, встречающиеся при хронометраже Java-кода. Так как анализ производительности требует понимания того, что делает приложение, в главе 3 приводится обзор некоторых средств мониторинга приложений Java.

После этого наступает момент для углубленного анализа быстродействия. Мы начнем с самых распространенных аспектов оптимизации: JIT-компиляции (глава 4) и уборки мусора (главы 5 и 6). В последующих главах внимание будет сосредоточено на оптимальном использовании различных частей платформы Java: использовании памяти в куче Java (глава 7), низкоуровневом использовании памяти (глава 8), быстродействии многопоточных приложений (глава 9), серверных технологиях Java (глава 10), работе с базами данных (глава 11) и общих рекомендациях Java SE API (глава 12).

В приложении приведен список всех флагов оптимизации, рассмотренных в книге, с указанием глав, где можно найти более подробное описание.

Платформы и соглашения

Хотя эта книга посвящена производительности Java, на производительность влияет несколько факторов: прежде всего, это версия Java, а также аппаратная и программная платформа, на которой она работает.

Платформы Java

В этой книге вопросы производительности рассматриваются на примере Oracle HotSpot JVM (Java Virtual Machine) и JDK (Java Development Kit) версий 8 и 11. Также эта комбинация обозначается термином Java SE (Standard Edition). Исполнительная среда JRE (Java Runtime Environment) представляет собой подмножество JDK, содержащее только JVM, но поскольку инструменты из JDK важны для анализа производительности, в этой книге центральное место занимает JDK. В практическом плане это означает, что в ней также рассматриваются платформы, созданные на базе от репозитория OpenJDK этой технологии, включающие JVM, опубликованные в проекте AdoptOpenJDK. Строго говоря, двоичные файлы Oracle требуют лицензии для коммерческого использования, а двоичные файлы AdoptOpenJdK поставляются на условиях лицензии с открытым исходным кодом. Для наших целей эти две версии будут рассматриваться как единое целое, которое мы будем обозначать термином JDK, или платформа Java2.

Для этих версий был выпущен целый ряд обновлений с исправлениями ошибок. На момент написания книги текущей версией Java 8 была версия jdk8u222 (версия 222), а текущей версией Java 11 — версия 11.0.5. Важно использовать как минимум эти версии (или более поздние), особенно в случае Java 8. Ранние выпуски Java 8 (приблизительно до jdk8u60) не содержат многие важные улучшения в области производительности и функциональные возможности, которые рассматриваются в книге (особенно в отношении уборки мусора и уборщика мусора G1).

Эти версии JDK были выбраны из-за того, что они пользуются долгосрочной поддержкой (LTS, Long-Term Support) от Oracle. Сообществу Java предоставлено право разрабатывать собственные модели поддержки, но до настоящего времени следовали модели Oracle. Таким образом, эти версии будут поддерживаться и оставаться доступными в течение некоторого времени: по крайней мере до 2023 года для Java 8 (через AdoptOpenJDK; позднее для продленных контрактов поддержки Oracle) и по крайней мере до 2022 года для Java 11. Ожидается, что следующая долгосрочная версия будет выпущена в конце 2021 года.

Что касается промежуточных версий, в обсуждение Java 11 были включены возможности, которые были изначально доступны в Java 9 или Java 10, несмотря на то, что эти версии не поддерживаются как компанией Oracle, так и сообществом в целом. При обсуждении этих возможностей я выражаюсь несколько неточно; может показаться, что я говорю, будто функции X и Y появились в Java 11, тогда как они были доступны в Java 9 или 10. Java 11 стала первой LTS-версией, поддерживающей эти возможности, и важно именно это: так как Java 9 и 10 не используются, на самом деле не так важно, где впервые появилась та или иная возможность. Аналогичным образом, хотя к моменту выхода книги уже появится Java 13, в книге Java 12 и Java 13 почти не рассматриваются. Вы можете использовать эти версии в своих приложениях, но только в течение полугода, после чего необходимо будет обновиться до новой версии (так что к тому моменту, когда вы будете читать эти слова, Java 12 уже не поддерживается, а версия Java 13, если и поддерживается, скоро будет заменена Java 14). Я расскажу о некоторых возможностях этих промежуточных версий, но поскольку в большинстве сред они вряд ли будут задействованы в рабочем коде, основное внимание будет уделяться Java 8 и 11.

Существуют и другие реализации спецификации языка Java, включая ответвления реализации с открытым кодом. Одну из них предоставляет AdoptOpenJDK (Eclipse OpenJ9), другие доступны от других разработчиков. Чтобы иметь возможность использовать имя Java, все эти платформы должны пройти тест на совместимость, однако эта совместимость не всегда распространяется до аспектов, рассматриваемых в книге. Это относится в первую очередь к флагам оптимизации. В любую реализацию JVM входит один или несколько уборщиков мусора (GC), но флаги для настройки реализаций GC от каждого разработчика зависят от конкретного продукта. Таким образом, хотя основные концепции книги актуальны для любой реализации Java, конкретные флаги и рекомендации относятся только к HotSpot JVM.

Данное предупреждение относится и к ранним версиям HotSpot JVM — флаги и их значения по умолчанию изменяются от версии к версии. Флаги, рассмотренные в тексте, действительны для Java 8 (конкретно версии 222) и 11 (конкретно 11.0.5). В более поздних версиях часть информации может измениться. Всегда проверяйте сопроводительную документацию новых версий — в ней могут быть описаны важные изменения.

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

В оставшейся части книги термины Java и JVM следует понимать как относящиеся к конкретной реализации Oracle HotSpot. Строго говоря, фраза «JVM не компилирует код при первом выполнении» неточна — некоторые реализации Java компилируют код при первом выполнении. Использовать эту сокращенную запись намного проще, чем постоянно писать (и читать) «Oracle HotSpot JVM…».

Флаги оптимизации JVM

JVM (с немногочисленными исключениями) получает флаги двух типов: логические флаги и флаги с параметром.

Логические флаги используют следующий синтаксис: -XX:+ИмяФлага устанавливает флаг, а -XX:-ИмяФлага сбрасывает его.

Флаги с параметром используют синтаксис -XX:ИмяФлага=значение; в этом случае ИмяФлага присваивается указанное значение. В тексте книги конкретное значение флага обычно заменяется чем-то, указывающим на произвольное значение. Например, -XX:NewRatio=N означает, что флагу NewRatio будет присвоено произвольное значение N (а последствия выбора N станут темой обсуждения).

Значения по умолчанию для всех флагов рассматриваются при первом упоминании флага. Значение по умолчанию часто определяется несколькими факторами: платформой, на которой работает JVM, а также аргументами командной строки JVM. Если у вас возникнут сомнения, в разделе «Основная информация VM» на с. 87 показано, как использовать флаг -XX:+PrintFlagsFinal (по умолчанию false) для определения значения по умолчанию конкретного флага в конкретной среде для заданной командной строки. Процесс автоматической оптимизации флагов в зависимости от параметров среды называется эргономикой.

Реализация JVM, загруженная с сайтов Oracle и AdoptOpenJDK, называется рабочей сборкой JVM. При сборке JVM из исходного кода можно построить много разных вариантов сборки: отладочные сборки, сборки для разработчиков и т.д. Такие сборки часто содержат дополнительную функциональность. В частности, сборки для разработчиков включают расширенный набор флагов оптимизации, чтобы разработчики могли поэкспериментировать с самыми незначительными аспектами различных алгоритмов, используемых JVM. Такие флаги обычно в книге не рассматриваются.

Аппаратные платформы

При выходе первого издания книги ситуация с оборудованием была совсем не такой, как сегодня. Многоядерные машины были популярными, но 32-разрядные платформы и однопроцессорные платформы все еще были широко распространены. Другие платформы, используемые в наше время, — виртуальные машины и программные контейнеры — еще только занимали свое место. Ниже приведен краткий обзор того, как эти платформы влияют на материал книги.

Многоядерное оборудование

Процессоры практически всех современных машин содержат несколько ядер, которые JVM (или любая другая программа) воспринимает как разные процессоры. Обычно все ядра включаются в систему гиперпоточного использования. Компания Intel предпочитает термин «гиперпоточность» (hyper-threading), хотя AMD (и другие) используют термин «одновременная многопоточность» (simultaneous multithreading), а некоторые производители микросхем говорят об «аппаратных цепях выполнения» в отдельных ядрах. Все это одно и то же, и я буду называть эту технологию «гиперпоточностью».

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

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

Таким образом, все выглядит так, словно 8-ядерная машина с гиперпоточностью может выполнять команды из 8 потоков одновременно (хотя технически она может выполнять только четыре команды на такт процессора). С точки зрения операционной системы, а следовательно, Java и других приложений, машина оснащена восемью процессорами. Тем не менее не все эти процессоры равны с точки зрения производительности. Если запустить одну счетную задачу, создающую интенсивную нагрузку на процессор, она будет использовать одно ядро; вторая счетная задача будет использовать второе ядро, и так далее до четырех. Вы можете запустить четыре независимые счетные задачи и добились четырехкратного повышения быстродействия.

Если добавить пятую задачу, она сможет выполняться только во время приостановки одной из остальных задач, что в среднем происходит где-то от 20% до 40% времени. Каждая дополнительная задача сталкивается с той же проблемой. Таким образом, добавление пятой задачи приведет к повышению производительности всего около 30%; в конечном итоге восемь процессоров обеспечат повышение производительности только в 5–6 раз по сравнению с одним ядром (без гиперпоточности).

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

Программные контейнеры

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

Особенно важны два контейнера. Первый — виртуальная машина, которая создает полностью изолированную копию операционной системы на подмножестве оборудования, на котором запускается виртуальная машина. Эта концепция лежит в основе облачных вычислений: у вашего провайдера облачных вычислений имеется вычислительный центр с очень мощными машинами. Такие машины могут иметь до 128 ядер, хотя обычно их меньше по соображениям эффективности затрат. С точки зрения виртуальной машины это не столь важно: виртуальная машина получает доступ к подмножеству этого оборудования. А следовательно, отдельная виртуальная машина может содержать два ядра (и четыре логических процессора, поскольку ядра обычно являются гиперпоточными) и 16 Гбайт памяти.

С точки зрения Java (и других приложений) эта виртуальная машина неотличима от обычной машины с двумя ядрами и 16 Гбайт памяти. Для целей оптимизации и анализа производительности достаточно рассматривать ее именно с этих позиций.

Второй контейнер, заслуживающий внимания, — контейнер Docker. Процесс Java, работающий в контейнере Docker, может не знать, что он находится в таком контейнере (хотя и может определить этот факт средствами инспекции), но контейнер Docker представляет собой всего лишь процесс (возможно, с ограниченными ресурсами) внутри работающей ОС. Как следствие, уровень его изоляции от использования процессоров и памяти другими процессами несколько отличается. Как вы вскоре увидите, подход к решению этой проблемы отличается в ранних версиях Java 8 (до обновления 192) и более поздней версии Java 8 (и всех версиях Java 11).

Избыточное резервирование виртуальных машин

Провайдеры облачных вычислений имеют возможность осуществить избыточное резервирование виртуальных машин на физическом оборудовании. Допустим, физическая машина оснащена 32 ядрами; провайдер обычно решает развернуть на ней восемь 4-ядерных виртуальных машин, чтобы каждая виртуальная машина имела четыре выделенных ядра.

Для экономии провайдер также может развернуть шестнадцать 4-ядерных вир­туальных машин. Теоретически маловероятно, чтобы все 16 виртуальных машин были заняты одновременно; если занята только половина из них, то физических ядер будет достаточно для удовлетворения их потребностей. Но если занятых машин окажется слишком много, они начнут конкурировать за ресурсы процессоров и их быстродействие пострадает.

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

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

По умолчанию контейнер Docker может свободно использовать все ресурсы машины: все доступные процессоры и всю доступную память на машине. И это нормально, если вы хотите использовать Docker исключительно для упрощения развертывания единственного приложения на машине (то есть на машине работает только этот контейнер Docker). Но довольно часто требуется развернуть несколько контейнеров Docker и ограничить ресурсы для каждого контейнера. Допустим, на 4-ядерной машине с 16 Гбайт памяти мы хотим запустить два контейнера Docker, каждому из которых доступны два ядра и 8 Гбайт памяти.

Настроить Docker для этой цели несложно, но на уровне Java могут возникнуть затруднения. Многочисленные ресурсы Java настраиваются автоматически (или эргономически) в зависимости от размера машины, на которой работает JVM. К их числу относится размер кучи по умолчанию и количество потоков, используемых уборщиком мусора (подробнее см. в главе 5), а также некоторые параметры пула потоков, упоминаемые в главе 9.

Если вы используете новую версию Java 8 (версия 192 и выше) или Java 11, то JVM поступит так, как вы ожидаете: если ограничить контейнер Docker для использования только двух ядер, значения, заданные эргономически на основании количества процессоров на машине, определяются с учетом ограничений контейнера Docker3. Аналогичным образом размер кучи и другие параметры, которые по умолчанию определяются на основании объема памяти на машине, основаны на ограничениях памяти, определенных для контейнера Docker.

В ранних версиях Java 8 JVM не располагает информацией об ограничениях, устанавливаемых контейнером: в процессе анализа окружения для определения объема доступной памяти и вычисления размера кучи по умолчанию JVM видит всю память на машине (вместо объема памяти, разрешенной для использования контейнеру Docker, как бы вам хотелось). При проверке количества доступных процессоров для оптимизации уборки мусора JVM видит все процессоры на машине вместо количества, выделенного контейнеру Docker. В результате JVM будет работать субоптимально: она запускает слишком много потоков и выделяет слишком большую кучу. Превышение количества потоков ведет к некоторому снижению производительности, но настоящая проблема кроется в использовании памяти: максимальный размер кучи может превысить объем памяти, выделенной контейнеру Docker. Когда куча увеличится до этого размера, контейнер Docker (а следовательно, и JVM) будет уничтожен.

В ранних версиях Java 8 параметры использования памяти и процессоров можно задать вручную. Когда в книге будут встречаться такие настройки, я буду особо отмечать те, которые необходимо отрегулировать в такой ситуации, но лучше просто обновиться до более новой версии Java 8 (или Java 11).

Контейнеры Docker создают одну дополнительную проблему для Java: в поставку Java включен обширный набор инструментов для диагностики проблем с производительностью. Часто они недоступны в контейнерах Docker. Эта проблема будет более подробно рассмотрена в главе 3.

Производительность: общая картина

В книге рассказано прежде всего о том, как лучше использовать API платформы Java и JVM, чтобы программы работали быстрее. На производительность также влияют многие внешние факторы. Эти факторы время от времени упоминаются в книге, но поскольку они не относятся к специфике Java, они не всегда обсуждаются подробно. Производительность JVM и платформы Java — лишь небольшой шаг на пути к высокой производительности выполнения.

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

Пишите более качественные алгоритмы

Многие подробности Java влияют на производительность приложения, и многие флаги оптимизации описаны в книге. Но никакого волшебного флага -XX:+RunReallyFast нет.

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

Хороший алгоритм — самый важный фактор высокой производительности.

Пишите меньше кода

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

Все это понятно, но проблема в том, что маленькая хорошо написанная программа выполняется быстрее большой хорошо написанной программы. Это относится ко всем компьютерным программам вообще и к Java-программам в частности. Чем больше кода приходится компилировать, тем больше времени пройдет, прежде чем этот код начнет работать быстро. Чем больше объектов приходится создавать и уничтожать, тем больше работы придется выполнять уборщику мусора. Чем больше объектов выделяется и хранится в памяти, тем больше времени занимает цикл уборки мусора. Чем больше классов загружается с диска в JVM, тем больше времени занимает запуск программы. Чем больше кода выполняется, тем меньше вероятность того, что он поместится в аппаратных кэшах на машине. И чем больше кода выполняется, тем больше времени займет выполнение.

на пути к поражению

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

Представьте, что вам приходится работать с интерфейсом Windows 10 на компьютере, который использовался для работы с Windows 95. Моим любимым компьютером всех времен был Mac Quadra 950, но на нем не работала macOS Sierra (причем даже если бы работала — она была бы очень, очень медленной по сравнению с Mac OS 7.5). На более низком уровне может показаться, что Firefox работает быстрее более ранних версий, но все эти версии по сути были второстепенными выпусками одного продукта. Firefox со всеми своими возможностями — поддержкой вкладок при просмотре, синхронизацией прокрутки и средствами безопасности — намного мощнее любой версии Mosaic, но Mosaic загружает HTML-файлы с жесткого диска приблизительно на 50% быстрее Firefox 69.0.

Конечно, Mosaic не сможет загружать реальные URL-адреса практически ни с одного популярного сайта; использовать Mosaic как основной браузер уже не получится. И этот факт тоже подтверждает более общую точку зрения: код (особенно между второстепенными выпусками) может оптимизироваться и выполняться быстрее. Мы, специалисты по производительности, должны сосредоточиться именно на этой задаче, и если мы достаточно хорошо справляемся со своим делом — сможем добиться успеха.

И этот момент очень важен; я вовсе не хочу сказать, что мы не должны работать над повышением производительности существующих приложений. Но как ни парадоксально, с добавлением новых возможностей и поддержкой новых стандартов (а это необходимо для того, чтобы не отстать от конкурирующих продуктов) программы обычно увеличиваются в размерах и начинают медленнее работать.

Я называю это принципом «смерти от тысячи порезов». Разработчик скажет, что всего лишь добавляет очень незначительную возможность, и на это времени вообще не потребуется (особенно если эта возможность не используется). А потом другие разработчики в том же проекте говорят то же самое, и внезапно производительность ухудшается на несколько процентов. Цикл повторяется в следующей версии, отчего общая производительность снижается на 10%. Пару раз в процессе разработки тестирование производительности может обнаружить превышение некоторого порога использования ресурсов — критическую точку в использовании памяти, переполнение кэша команд или что-нибудь в этом роде. В таких случаях обычные тесты производительности выявляют это конкретное условие, а группа производительности исправляет то, что выглядит как серьезная регрессия. Но со временем в проект проникает все больше мелких регрессий, а исправлять их становится все труднее и труднее.

Это вовсе не означает, что вы не должны добавлять в свой продукт новые возможности или новый код; разумеется, совершенствование программ приносит пользу. Но вы должны знать о тех компромиссах, на которые вам придется пойти, и упрощать свой код там, где это возможно.

Применяйте преждевременную оптимизацию

Термин «преждевременная оптимизация» часто приписывают Дональду Кнуту. Обычно разработчики используют этот термин, чтобы заявить, что от эффективности их кода ничего не зависит, а если и зависит — они все равно не узнают об этом, пока код не заработает. Полная цитата, если она вдруг вам еще никогда не попадалась, звучит так: «Преждевременная оптимизация — корень всех зол»4.

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

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

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

log.log(Level.FINE, "I am here, and the value of X is "

        + calcX() + " and Y is " + calcY());

Код выполняет конкатенацию строк, которая, скорее всего, окажется излишней, потому что сообщение будет выводиться в журнал только при достаточно высоком уровне протоколирования. Если сообщение не выводится, вызовы методов calcX() и calcY() также будут избыточными. Опытные Java-разработчики инстинктивно отказываются от таких решений; некоторые IDE даже помечают код и предлагают изменить его. (Впрочем, инструментарии не идеальны: скажем, NetBeans IDE помечает конкатенацию, но в предлагаемом улучшении сохраняются лишние вызовы методов.)

Код вывода сообщений в журнал лучше записать в следующем виде:

if (log.isLoggable(Level.FINE)) {

    log.log(Level.FINE,

            "I am here, and the value of X is {} and Y is {}",

            new Object[]{calcX(), calcY()});

}

В этом случае конкатенация вообще не задействована (формат сообщения не обязательно будет более производительным, но он проще), а вызовы методов или выделение памяти под массив объектов не будут выполняться при отключенном протоколировании.

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

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

Ищите в других местах: база данных всегда является узким местом

Если вы разрабатываете автономные приложения Java, которые не используют внешние ресурсы, для вас важна (в целом) только производительность самого приложения. При добавлении внешнего ресурса (например, базы данных) приходится учитывать производительность обеих программ. А в распределенной среде — допустим, с REST-сервером Java, распределителем нагрузки, базой данных и информационной системой предприятия — производительность сервера Java может оказаться наименьшей из проблем с производительностью.

Ошибки и проблемы с производительностью  не ограничиваются JVM

В этом разделе проблемы производительности рассматриваются на примере базы данных, но источником этих проблем может стать любая часть среды.

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

После того как многие потенциальные причины были отклонены, проблема оставалась, причем у нас не было базы данных, которую можно было обвинить в происходящем. Следующей наиболее вероятной причиной была тестовая конфигурация, а профилирование определило, что источником регрессии был генератор нагрузки Apache JMeter: каждый ответ сохранялся в списке, а при поступлении нового ответа он обрабатывал весь список для вычисления 90% времени отклика (если этот термин вам не знаком, обращайтесь к главе 2).

Проблемы с производительностью могут быть обусловлены любой частью системы, в которой развернуто приложение. Анализ типичного случая рекомендует начать с рассмотрения самой новой части системы (которой часто оказывается приложение в JVM), но вы должны быть готовы рассматривать все возможные компоненты среды.

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

С другой стороны, вы не должны забывать об этом исходном анализе. Если база данных является узким местом (подсказка: это частое явление), оптимизация приложения Java, работающего с базой данных, не улучшит общего быстродействия. Более того, она может привести к противоположному результату. Как правило, при повышении нагрузки в перегруженной системе производительность такой системы ухудшается. Если изменения в приложении Java сделают его более производительным — что только повысит нагрузку на уже перегруженную базу данных, — общая производительность может ухудшиться. И тогда вы можете прийти к ошибочному выводу, что от конкретного улучшения JVM лучше отказаться.

Этот принцип — повышение нагрузки на компонент в плохо работающей системе приводит к замедлению всей системы — не ограничивается базой данных. В частности, он действует при добавлении на сервер нагрузки, требующей интенсивных вычислений, или если большее количество потоков начнет запрашивать блокировку, которую уже ожидают другие потоки, а также в множестве других сценариев. Яркий пример такого рода, в котором задействована только JVM, приведен в главе 9.

Оптимизация для типичного случая

Появляется искушение — особенно с учетом синдрома смерти от тысячи порезов — рассматривать все аспекты производительности как одинаково важные. Однако вы должны сосредоточиться на типичных сценариях использования. Этот принцип имеет несколько проявлений:

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

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

Пишите простые алгоритмы для самых распространенных операций в приложении. Допустим, программа вычисляет результат по математической формуле, а пользователь может выбрать, получить ли ответ с погрешностью 10% или 1%. Если большинству пользователей будет достаточно 10% погрешности, оптимизируйте эту ветвь — даже если это приведет к замедлению кода, обеспечивающего 1%-ную погрешность.

Итоги

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

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

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

1 Один из так называемых «законов Кларка» — https://ru.wikipedia.org/wiki/Три_закона_Кларка. — Примеч. пер.

2 В отдельных редких случаях эти два понятия различаются; например, в версиях Java из AdoptOpenJDK присутствуют новые уборщики мусора из JDK 11. Я буду указывать на эти различия там, где они будут актуальны.

3 Ограничениям процессоров в Docker можно присвоить дробные значения. Java округ­ляет все дробные значения до ближайшего целого.

4 Нет полной ясности относительно того, кто сказал это впервые: Дональд Кнут или Топи Хоар (Topy Hoare). Но данное высказывание встречается в статье Кнута «Structured Programming with goto Statements». И в контексте оно может рассматриваться как аргумент в пользу оптимизации кода, даже если для этого требуются такие неэлегантные решения, как команды goto.