Как мы готовим Axiom JDK
Привет, Хабр!
Подготовка JDK — это не просто запустить скрипт и получить готовый бинарник. Это сложный процесс, включающий тестирование, поддержку, оптимизацию и обеспечение безопасности.
Подготовка дистрибутива состоит не только из одноразовой сборки, что само по себе является нетривиальной задачей, если приходится поддерживать множество платформ и конфигураций.
В этой статье расскажем, как мы готовим наш продукт Axiom JDK и с какими проблемами сталкивались по пути.
JDK можно собрать вручную, но делать это в проде — так себе идея. Вот почему:
Новые версии выходят 6+ раз в год, и каждую придётся компилировать заново.
Поддержка разных ОС и версий требует отдельных ресурсов.
В OpenJDK миллионы строк кода (OpenJDK 6 — ~6 млн строк, OpenJDK 8 — 11+ млн, OpenJDK 21 — 14+ млн), а с зависимостями ещё больше.
Мы поддерживаем версии JDK от 6 до самой свежей.
Поэтапный процесс подготовки продукта
Инфраструктура
Для сборки и тестирования у нас есть парк машин:
37 конфигураций машин для сборки,
64 конфигурации машин для тестирования,
машины для тестирования производительности,
машины для фаззинга и статического анализа.
Этап 0. Подготовка
Проект OpenJDK включает множество репозиториев. Каждая версия и её обновления обычно хранятся в отдельных репозиториях.
Например, OpenJDK 17 базируется на репозитории. Обновления релизов для 17 версии (от 17.0.1 и выше) разрабатываются и тестируются в репозитории https://github.com/openjdk/jdk17u, а разработка и тестирование изменений осуществляются в https://github.com/openjdk/jdk17u-dev.
Основная разработка ведётся в репозитории https://github.com/openjdk/jdk. Если необходимо, изменения переносятся в более ранние версии и соответствующие репозитории. Все эти процессы сопровождаются построением промежуточных версий OpenJDK и тестированием со стороны сообщества.
Наши репозитории устроены аналогичным образом, но разделение по версиям осуществляется через соответствующие ветки.
Некоторые репозитории проекта OpenJDK уже переведены в режим только для чтения, и в них нет активности, так как сообщество не поддерживает соответствующие версии.
Наши клиенты часто просят добавить исправления безопасности и функциональных улучшений в устаревшие версии Axiom JDK. Для этого мы выпустили версии, которые включают эти исправления.
Бывают ситуации, когда замена последней версии на новую невозможна из-за возможных проблем с совместимостью. Например, у вас есть версия 8uXXX с известной проблемой и вы понимаете, что следующая версия может привести к поломке вашего ПО (например, если вы исправили существующий баг, который несовместим с исправлением в OpenJDK). Поэтому вы не можете перейти на новую версию сразу после её выхода. Или вы хотите сначала протестировать все новые изменения в OpenJDK, прежде чем выкатывать версию в прод.
В таких случаях помогают CPU-релизы (Critical Patch Update) — обновления, в которых исправлены уязвимости безопасности без изменений функциональности. Если вы обновляете свою версию до CPU, вы получаете все исправления безопасности в день релиза с минимальной вероятностью возникновения проблем в рабочей среде, поскольку в этой версии больше нет никаких других изменений.
После обновления и тестирования вашего ПО с новой версией можно перейти на PSU-версию (Patch Set Update), которая включает все исправления безопасности после проведения тестирования.
Этап 1. Сборка бинарных файлов
Мы создаём сборки для различных платформ, версий и конфигураций:
Поддерживаемые платформы
Linux x86–64/AArch64 (6 вариантов),
macOS M1/x86–64,
Windows x86–32/x86–64/AArch64,
Solaris Sparcv9/x86–64 и другие.
Варианты сборок
Версии: Certified и Pro.
Варианты поставок:
Full*,
Lite*,
Standard*,
Express (сочетает в себе модифицированную виртуальную машину JVM и стандартную библиотеку JDK 8 или 11),
для контейнера Axiom Runtime Container,
для Axiom Linux.
Форматы: JRE и JDK.
Версии: 6 CPU, 7 CPU, 8 CPU, 8 PSU, 11 CPU, 11 PSU, 17 CPU, 17 PSU, 21 CPU, 21 PSU, 23 PSU.
Версии для Axiom Native Image Kit (Axiom NIK) — пакет разработчика с утилитой Native Image, преобразующей байт-код на языках JVM в предварительно скомпилированный исполняемый файл, который запускается автономно и почти моментально.
Версии с Coordinated Restore at Checkpoint (согласованное восстановление из контрольной точки, сокр. CRaC) — проект OpenJDK для сохранения контрольной точки и восстановления из неё состояния приложения для ускорения прогрева и запуска приложения.
На каждую сборку также создаётся несколько типов инсталляторов: .msi
, .deb
, .rpm
, .zip
и другие.
Разные версии продуктов требуют разные компиляторы, так как для разных версий есть разные целевые платформы. Например, для старых версий Linux приходится собирать код под определённую glibc.
В результате получается пара тысяч комбинаций сборок бинарных файлов.

Этап 2. Тестирование сборок бинарных файлов
Следующий этап — проверить, как собраны бинарные файлы (до сборки инсталляторов).
Мы проводим:
jtreg-тестирование — 20 000+ тестов на всех заявленных поддерживаемых платформах.
Собственные тесты на соответствие спецификации — 140 000 тестов.
Итого: 160 000 тестов для каждой платформы, под которую осуществляется сборка. Это нужно для того, чтобы проверить, что собранный проект соответствует спецификации и работает правильно, а именно:
нет критических вылетов и падений,
100% покрытие спецификации,
отсутствуют регрессии.
Эти 3 критерия позволяют говорить, что то, что мы построили, готово превращаться в конечный продукт.
Тестирование производительности (Performance testing)
Ещё одна часть работы над продуктом — тестирование производительности (performance testing). Когда нужно сравнить производительность по каким-либо показателям, тратится много времени для выявления точной картины.
Количество времени зависит от того, насколько детальную картину и насколько подробно нужно выяснять. Чтобы получить подробную картину, необходимы бенчмарки, которые зависят от разных подсистем: где-то узким местом является однопоточная производительность, где-то — количество и характеристики памяти, где-то — количество ядер и характеристики подсистем их взаимодействия и другие. Для кого-то интересно выяснить картину на новейшем оборудовании, а у кого-то основной парк машин отстаёт на несколько поколений.
Затем каждую интересующую нас комбинацию нужно умножить на количество конфигураций виртуальной машины (в большинстве случаев как минимум имеет смысл проводить замеры для разных GC). И если для замера запуска приложения вряд ли потребуется много времени, то для прочих замеров необходимо выяснить поведение того или иного приложения в процессе и после прогрева. В некоторых случаях это может достигать 2–3 часов для каждого запуска бенчмарка в каждой конфигурации виртуальной машины на каждом варианте машинной конфигурации/ОС/архитектуры.
С некоторой периодичностью мы проводим бенчмаркинг для нашего Axiom JDK Express. Далее расскажем как мы это делаем и как это нужно делать на примере Axiom JDK Express.
Главная сложность — количество замеров (= время). Решается она распараллеливанием. Разовая сложность — первая настройка конфигурации. Регулярная сложность — время, которого всегда нет.
Допустим, у нас наступил день релиза и мы хотим сравнить, как изменилась производительность по сравнению с предыдущим релизом.
Сколько конфигураций нужно протестировать?
Дано: в день релиза выходят JDK 8, JDK 11, JDK 17, JDK 21, JDK N, где N — последний на данный момент релиз.
Сравниваются предыдущие релизы со свежими:
JDK 8 предыдущий vs JDK 8 свежий,
JDK 11 предыдущий vs JDK 11 свежий и т.п.
В рамках каждого из таких сравнений сравниваются как минимум две платформы в качестве основных — Linux x86 (64 бит) и Linux AArch64. Если есть необходимость, добавляются и все остальные: Linux x86 (32 бит), Linux ARM, Linux PPC, Linux RISC-V, macOS x86 (64 бит), macOS AArch64, Windows x86 (64 бит), Windows AArch64. Это не считая musl-билдов Linux, headless-билдов, билдов с JavaFX, билдов некоторых релизов для Solaris и других. Отсюда мы получаем 5 релизов Java по 2 платформы (в идеале 10 платформ).
Итого: от 5×2 до 5×10 конфигураций.
Для каждой из таких конфигураций нужно измерить разные настройки виртуальной машины (ВМ). Мы стараемся охватить хотя бы 3 основных сборщика мусора (Garbage Collector): Parallel GC, G1 GC, Z GC (по желанию клиента у каждого сборщика мусора можно попробовать разные настройки). Есть и реже используемые Serial GC, Shenandoah GC, Epsilon GC, а для старых JDK — Concurrent Mark Sweep GC. Так, полученные на предыдущем шаге 10–50 конфигураций умножаем ещё на 3–7 GC.
Итого: от 30 до 350 конфигураций.
Также следует измерить запуск с включенным и выключенным CDS. Следовательно, умножаем число конфигураций ещё на 2. Получаем от 60 до 700 конфигураций. Ещё нужно измерить с включенным и выключенным appCDS. Тут умножаем на полтора, так как не бывает такой конфигурации, в которой CDS выключен, а appCDS включен.
Итого: от 90 до 1050 конфигураций.
Теперь каждая из 90–1050 конфигураций проходит бенчмаркинг.
Бенчмарки
Бенчмарки и приложения, используемые в качестве источника телеметрии:
SPECjvm98: занимает около 20 часов, чтобы запустить на JDK предыдущем и на JDK следующем;
SPECjbb: около 8 часов для двух билдов;
DaCapo: около 2 часов для двух сравниваемых билдов;
PetClinic: около 3 часов на билд, итого: 6 часов;
BigRamTester: хотя бы 1 час на билд, итого: 2 часа;
Microbenchmarks (*): от 24 до 48 часов в зависимости от конкретной версии Java и оборудования.
(*) Microbenchmarks — набор бенчмарков для измерения отдельных элементарных операций или же небольших групп таких операций, например, чтение или запись поля определенного типа у объекта.
Для каждой из 90–1050 конфигураций потребуется в среднем 74 часа. Это от 6660 до 77 700 часов — от 277 до 3237 дней машинного времени. Да, что-то можно распараллелить, но парк машин ограничен. Желательно запускать одну платформу на одной и той же физической машине, чтобы не сравнивать цифры, полученные на разных моделях оборудования.
Релиз раз в 3 месяца, то есть раз в 90 дней. Следовательно, нужно потратить от 277 до 3237 дней машинного времени, чтобы всё измерить. Предположим, мы всё распараллелили (это затратно, трудно, но можно) и получили массивы цифр и графиков для 90–1050 конфигураций.
Бонус: ещё иногда может возникнуть необходимость отдельно измерить Client VM, Server VM и Minimal VM. Поэтому числа сверху умножаем ещё на 3)
Как визуализировать и анализировать полученные данные?
После тестов у нас десятки гигабайт телеметрии. Чистый ад, если их не визуализировать.
Вы когда-нибудь сравнивали мегабайт телеметрии, умноженный на 1050 конфигураций? Или хотя бы на 90 (при том, что «Война и мир» Л.Н. Толстого занимает около 8 Мбайт)?
Для визуализации данных мы используем Grafana. Вот пример графика визуализации данных для бенчмарка DaCapo. Он показывает время выполнения (чем меньше, тем лучше) в миллисекундах для Standard-билда Axiom JDK на 3 разных ОС: Axiom Linux (glibc), Alpine Linux 3.21, Debian 12.

Этот график — лишь кусочек данных.
И даже после всех этих манипуляций JDK ещё не готов. Его нужно превратить в дистрибутив, который можно выкатывать и поддерживать.

Фантастические баги и где они обитают
В любой момент можно столкнуться с багами, которые нужно суметь найти и устранить. Ниже мы привели примеры багов, с которыми столкнулись сами.
Баг во взаимодействии JVM с ОС
Иногда баги возникают не в коде JVM, а в том, как она взаимодействует с ОС. Один из таких случаев — ошибка в OpenJDK, из-за которой JVM могла неправильно определять число доступных процессоров на Linux-системах.
Проблеме были подвержены все дистрибутивы Linux, базирующиеся на glibc версии ниже 2.34 (Ubuntu 18.04, Centos 8 и другие) и на musl версии ниже 0.9.12 (Alpine и другие). Также при количестве доступных процессоров более 127 на musl любой версии могло появляться некорректное диагностическое сообщение о возможном превышении лимита процессоров (Alpine и другие).
В этом случае Java выводила следующее предупреждение в стандартный поток ошибок:
OpenJDK 64-Bit Server VM warning: sched_getaffinity failed (Invalid argument)- using online processor count (144) which may exceed available processors
и игнорировала заданную маску процессоров. Отсюда Java некорректно определяла количество доступных процессоров. Java считала, что доступны все процессоры в системе, что приводило к настройке неправильной эргономики сборщика мусора и проблемам с производительностью.
Баги в JVM
Можно наткнуться и на баги в виртуальной машине (например, проблемы с кодированием CAS-инструкций в AArch64), которые прячутся так, что их видно только в debug-билде. Debug-билд — отдельный вариант билда, для которого генерируются debug-символы, а также включены дополнительные предупреждения (assert) для проверок критичных мест.
В release-билде виртуальная машина продолжает «молча» пытаться работать дальше и в ряде случаев «ломается» дальше в случайном месте.
Баги в глубине Java Class Library (JCL)
Есть вероятность столкнуться со спецификой смены логики работы некоторых деталей в глубине библиотеки Java-классов (Java Class Library, JCL), приводящих к замедлению исполнения отдельных участков кода в 100 раз, например, JAXP Plugability Layer should use java.util.ServiceLoader. Из-за этого бага при большом количестве библиотек в classpath ServiceLoader на виртуальной машине с настройками по умолчанию начинает сканировать классы по этим библиотекам и тормозит в разы сильнее.
Мы разобрались с этой проблемой и выработали решение. Затем рассказали клиенту, как и что нужно настроить, чтобы обойти этот баг. В данном случае проблема решалась специфическими настройками JDK.
Баги, зависящие от конкретного оборудования
Есть баги, которые проявляются только при наличии специфического оборудования. Например, ARM32 SIGILL issue on single core CPU (not supported PLDW instruction).
Этап 3. Сборка инсталляторов, подписывание и тестирование на целевых конфигурациях
После тестирования мы собираем инсталляторы, подписываем их и тестируем на целевых платформах.
Сборка инсталляторов для одной версии Axiom JDK, например, 11 может занимать от 2 до 6 часов, в зависимости от того, под какие платформы собирается дистрибутив.
Затем идёт добавление электронной подписи для дистрибутивов и файлов в составе дистрибутивов на ОС, где есть такая поддержка. Например, macOS, Windows и некоторые варианты Linux, например Alt Linux и Astra Linux.
Анализ сертифицированной версии Axiom JDK Certified
Для сертифицированной версии проводятся различные виды анализа безопасности, в том числе динамический и статический.
При динамическом анализе выполняется фаззинг-тестирование самых разных компонентов, в первую очередь имеющих прямое или косвенное отношение к уровню защищенности JDK. Также при динамическом анализе выполняется большой набор тестов JDK на JVM, инструментированной санитайзерами ASAN (AddressSanitizer) и UBSAN (Undefined Behavior Sanitizer). После такого тестирования проводится отдельная работа по парсингу и затем ручному анализу отчётов санитайзеров.
Статический анализ выполняется посредством статического анализатора Svace и многих рабочих часов наших лучших инженеров, занятых разметкой предупреждений Svace.
Помимо этого выполняются и другие проверки, в их числе:
анализ инсталляторов (access_check и его аналог на Linux),
анализ сборки бинарных файлов (binskim),
антивирусы (Kaspersky, Dr Web),
функциональные тесты для функций безопасности.
В сертифицированную версию также добавляются новые функции безопасности. Например, недавно мы добавили поддержку ЗПС для Java. Для добавления этой функции мы провели исследование, целью которого был поиск всех мест в кодовой базе Java, откуда выполняются прямые или косвенные обращения к таким системным вызовам как open
, openat
и прочим.
Заключение
Чтобы создать продукт с поддержкой, требуется постоянная работа целой команды разработчиков.
Сборка дистрибутива — это разовая история, которая в простом сценарии может быть быстро реализована для проверки новых изменений и исправлений в ручном режиме. Для промышленных масштабов разработки требуется дополнительно обеспечить поддержку инфраструктуры для сборки и тестирования. Это кратно усложняет процесс для проведения всех этапов, обеспечивающих гарантию качества конечного продукта:
сборка OpenJFX,
сборка OpenJDK,
тестирование,
сборка дистрибутивов для распространения.
Всё это требует постоянной работы команды инженеров, автоматизированных процессов и глубокой экспертизы.
*Full — полный комплект поставки, включает AxiomFX на базе OpenJFX и Minimal VM.
Standard — стандартный вариант, подходит для большинства серверов или десктопов, где не требуются дополнительные компоненты.
Lite — оптимизированный вариант для облачных систем. Включает следующие улучшения:
сокращение времени, которое JVM проводит в safepoints;
возможность возврата памяти, выделенной JVM;
возможность дедупликации строк при работе сборщиков мусора (GC).