openjdk.ruOpenJDK на русском

JEP draft: CDS Implementation Notes

Заметки о реализации CDS

AuthorsJohn Rose, Ioi Lam, Dan Heidinga
ОтветственныйJohn Rose
ТипInformational
ОбластьImplementation
СтатусDraft
Компонентhotspot / runtime
Создан2024/07/11 06:15
Обновлён2024/07/18 00:06
Задача8336232

Описание

Этот JEP содержит техническую информацию о Cache Data Store (CDS) виртуальной машины Java: о его понятиях, внутренних операциях и текущих ограничениях. Этот материал задуман не как учебное руководство, а как подробный справочник, и поэтому предполагает знакомство с основными понятиями CDS.

Учебные материалы по CDS можно найти в других местах, например здесь:

  • Краткое введение в CDS в составе мотивации [Ahead-of-Time (заблаговременная компиляция и подготовка) Class Linking JEP].
  • FIXME (Здесь нужно больше материала.)

Содержание заметок о реализации

Как CDS поддерживает динамичность Java

Java-приложения можно надёжно и легко собирать из огромного набора библиотек и фреймворков, а также легко, без лишних формальностей, настраивать для тестирования и развёртывания. Программистам доступны быстрые циклы разработки, удобная наблюдаемость и мощный набор инструментов. Основа всего этого — пара «суперспособностей» Java: раздельная компиляция и динамическое связывание. Классами можно управлять и исследовать их по отдельности, изучая их class-файлы. Когда классы объединяются динамическим связыванием, их целостность защищает VM, и при этом VM даёт им высокопроизводительный доступ к точкам API друг друга. (К таким точкам API относятся поля и методы, доступ к которым выполняется как через рефлексию, так и напрямую из байт-кода.) Важно, что конфигурация приложения естественным образом складывается из классов, представленных во время выполнения, по мере того как они связываются друг с другом; на этапе сборки не требуется никакой «церемонии связывания», чтобы исчерпывающе определить конфигурацию приложения. Большинство механических шагов конфигурирования Java-приложения выполняется на лету, незаметно для программиста.

Отчасти это работает потому, что Java, несмотря на статическую типизацию, — очень динамичный язык: загрузка, связывание, генерация машинного кода и освобождение памяти — вот некоторые из его динамических механизмов. Вся эта динамичность даёт программисту большую гибкость, но имеет цену на низком уровне. При каждом выполнении приложения приходится снова и снова повторять одну и ту же работу: каждый раз находить нужные байты class-файла для заданного имени класса, нужные адреса методов или полей, нужные вспомогательные данные среды выполнения или нужный машинный код для оптимизации приложения. Это повторение неизбежно в современных Java VM, пока они выполняют большинство своих операций лениво, в момент необходимости (just in time). Динамичность позволяет откладывать вычисляемые решения до последнего момента; динамичность позволяет организовать загрузку, связывание и оптимизацию как операции just in time, что даёт максимальную гибкость.

При развёртывании приложения многие из этих динамически вычисляемых решений уже стабилизировались, и можно ожидать, что их результат будет таким же, как в предыдущих запусках. Такая стабильность не отменяет динамичности. Если приложение в рабочей среде решит выполнить новое действие, которого раньше не ожидалось, VM может динамически отреагировать на изменение: возможно, загрузив новые классы, возможно, отбросив ранее оптимизированный код и данные, возможно, выполнив повторную оптимизацию. Такое непредсказанное поведение не грозит лишь самым маленьким и простым Java-приложениям, но обработка just in time, которую допускает динамичность, покрывает все возможные варианты в любом приложении.

Таким образом, общий набор решений по конфигурированию и оптимизации, которые принимает приложение (вместе с VM, которая его выполняет), во многих случаях предсказуем. Спецификация Java VM даёт большую свободу в планировании решений, как бы динамически они ни запрашивались. Непредсказанное решение всегда должно обрабатываться как услуга just in time, но предсказуемое можно обработать и заранее (ahead of time). Во многих случаях нетрудно предоставить AOT-ресурсы, выдавая их приложению без задержки всякий раз, когда они ему нужны. Чтобы перейти от JIT-обработки к AOT-обработке, нужна информация в виде предсказания — заранее известных решений, принимаемых для конфигурирования или оптимизации приложения. Предсказания не обязаны быть точными на 100 %, если есть способ восстановиться после ошибки предсказания. Часто самый прямой способ получить такие предсказания — выполнить обучающий запуск приложения и наблюдать за решениями, принятыми во время этого запуска. Если предположить, что похожие будущие запуски будут принимать похожие решения, VM может заранее подготовиться к их выполнению в следующем запуске. На этом основана технология CDS.

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

При создании архива CDS VM может спекулятивно предполагать, что предыдущие решения, записанные во время обучающего запуска, позже будут приняты так же. Если код приложения в рабочей среде примет другие решения, VM легко обнаружит новые требования. Например, если рабочему запуску понадобится другой набор классов, VM может просто обработать новые классы just in time, традиционным полностью динамическим способом, как если бы CDS вообще не участвовал. То же верно, если класс в рабочем запуске запрашивает связывание с каким-либо API (другим классом, методом или полем), которого не касался обучающий запуск: непредсказанное решение о связывании можно выполнить just in time. Всё это верно независимо от того, как приложение инициирует загрузку и связывание API — через байт-коды или через рефлексию. Во всех случаях гибкая динамичность Java в полной мере сосуществует со стабильными предсказаниями, хранящимися в архиве CDS.

Что содержится в файле архива CDS

Базовая возможность CDS — спекулятивно предсказывать решения о загрузке классов на основе обучающего AOT-запуска. В некоторых сценариях работы список классов, замеченных в обучающем запуске, экспортируется в файл списка классов (class-list), который затем собирается XX Также можно работать с текстовым списком выбранных классов, хотя при этом очень легко ошибиться.

Для каждого выбранного class-файла CDS может сохранить предварительно разобранную (или «законсервированную», pickled) внутреннюю форму как независимо загружаемый ресурс в файле архива CDS. Внутренняя форма по существу совпадает с внутренними метаданными классов VM. Её сопровождают «карты указателей» (pointer maps), которые описывают, как перемещать указатели, встроенные в метаданные, чтобы архив CDS можно было загрузить по непредсказуемым базовым адресам в виртуальной памяти рабочего запуска.

Но при старте VM, хотя все ресурсы CDS сразу доступны в памяти VM, их, возможно, ещё нельзя использовать как классы и нельзя связать друг с другом, если они находятся только в предварительно разобранном состоянии.

Когда Java-приложение в конце концов впервые запрашивает класс CDS, VM окончательно делает предварительно разобранную форму «живой» и связывает имя класса с живыми метаданными. Только после этого класс можно связать с другими загруженными классами. Это можно рассматривать как реализацию загрузки классов частично в AOT-порядке, частично в JIT-порядке.

С другой стороны, если архив собран с -XX:+AOTClassLinking, VM сама инициирует AOT-загрузку, помещая образы метаданных в системный словарь VM. Это происходит на очень раннем этапе, до того как начнёт выполняться метод main приложения, и поэтому называется фазой premain выполнения. В это время и загрузка, и связывание происходят быстро, из ресурсов CDS, уже находящихся в памяти VM и заранее отформатированных так, чтобы их было легко принять в качестве живых метаданных.

Благодаря тому, как ресурсы переносятся в память VM из архива CDS, у них стабильные и предсказуемые адреса в памяти. Эта стабильность, в свою очередь, позволяет заранее отформатировать их в уже связанном состоянии, с прямыми ссылками друг на друга. Если говорить конкретно, расширенное предварительное форматирование затрагивает элементы пула констант в ресурсе каждого класса: их можно заполнить разрешёнными адресами и размерами полей, методов и других классов, если эти сущности тоже присутствуют в классах, загруженных в AOT-порядке.

Таким образом, эти действия по AOT-загрузке и связыванию происходят быстрее, чем для классов, которые обрабатываются по частям при загрузке и связывании just in time. Но, прибегнув к оптимизации по принципу «как если бы» (as-if), можно также считать, что загрузка и связывание происходят just in time, по требованию приложения. Единственное свидетельство перехода от JIT-порядка к AOT-порядку косвенное: например, изменение активности файловой системы или сообщения журнала, которые выводит VM.

Когда оптимизация «как если бы» работает, приложение не может отличить связывание «заранее» (ahead of time) от связывания «по требованию» (just in time), кроме как по скорости. Такие правила «как если бы» — обычное дело в технологиях VM. Ещё один пример: код, скомпилированный VM, выполняется «так, как если бы» его выполнял интерпретатор VM, только быстрее. Также сборщик мусора позволяет приложению выделять память без ограничений, «как если бы» память была бесконечной.

Преимущество поведенческого сходства загрузки и связывания в JIT- и AOT-порядке состоит в том, что CDS по-прежнему может загружать или связывать некоторые классы приложения старым способом, чтобы обрабатывать граничные случаи, которые было бы неудобно загружать в новом AOT-порядке. Поэтому, хотя основная масса классов, скорее всего, будет заранее отформатирована в архиве CDS для AOT-загрузки, некоторые классы могут быть не в новой форме. Это позволяет CDS гибко работать с более открытыми возможностями VM, например с пользовательскими загрузчиками классов. Точно так же CDS может решить не задавать заранее какое-то отдельное решение о связывании даже в классе, загруженном в AOT-порядке, если у CDS есть основания считать, что это решение может измениться в рабочем запуске, или если CDS считает, что это будет напрасной тратой усилий. Все эти варианты прозрачны для приложения.

Присутствие в памяти VM множества классов приложения по предсказуемым («стабилизированным») адресам, вероятно, станет трамплином для дальнейших улучшений CDS. Дополнительные виды данных VM, например профили методов и скомпилированный код, можно хранить в архиве CDS как новые ресурсы, заранее отформатированные так, чтобы напрямую связываться с любыми нужными им классами, методами и полями.

Виды AOT-обработки

Разные версии CDS выполняют разный объём обработки в режиме Ahead-of-Time. Самые ранние версии CDS лишь заранее разбирают class-файлы, но не пытаются установить классы, пока логика приложения не сделает обычный запрос на загрузку класса «точно в срок» (just in time). Более поздние версии выполняют всё больший объём AOT-обработки.

Различные виды AOT-обработки включаются параметрами командной строки, заданными при создании файла архива CDS. Они сохраняются внутри файла архива CDS. Когда VM выполняет рабочий запуск и получает указание использовать определённый файл архива, она выполняет AOT-обработку, запрошенную этим файлом архива. Никаких других параметров командной строки или настроек конфигурации в рабочем запуске не требуется: всё берётся из архива CDS.

Некоторые виды обработки можно отключить, что может быть полезно для диагностики проблем. Например, -XX:-AOTClassLinking (обратите внимание на знак минус -) отключает загрузку и связывание классов. Это также отключит последующие AOT-оптимизации, если они есть, например AOT-компиляцию. Если рабочему запуску указано отключить AOT-загрузку, VM пытается вернуться к обработке ресурсов CDS как заранее разобранных классов, которые загружаются в традиционном порядке «точно в срок».

Параметр -XX:+AOTClassLinking помещает в Cache Data Store атрибут, который указывает VM сразу при запуске переводить кэшированные классы в загруженное состояние. Так классы, используемые приложением (как выяснил обучающий AOT-запуск), становятся доступны немедленно. Однако кэшированные классы, которые нельзя загрузить в AOT-порядке (например, классы с пользовательскими загрузчиками классов), загружаются только по требованию (то есть «точно в срок») из заранее разобранного состояния в CDS.

Параметр -XX:+AOTClassLinking также включает последующую AOT-обработку, а именно AOT-связывание классов, загруженных в AOT-порядке. Связываются только константы, ссылающиеся на другие классы, загруженные в AOT-порядке.

Константы классов, которые настраивают построение лямбд и логику конкатенации строк, связываются заранее. Для этого выполняются соответствующие bootstrap-методы invokedynamic и сохраняются ресурсы CDS, кодирующие получившиеся цепочки дескрипторов методов (method handles) и скрытых классов. Таким образом -XX:+AOTClassLinking поддерживает AOT-загрузку и AOT-связывание динамически генерируемых классов, а не только тех, что находятся в class path или графе модулей. Эта AOT-обработка bootstrap-методов ограничена методами в java.base, про которые известно, что у них нет побочных эффектов; (пока) её нельзя распространить на произвольные методы из сред выполнения других языков.

Ещё один вид AOT-обработки (в будущем) — сбор профилей с (будущим) флагом -XX:+AOTMethodProfiling. Он будет сохранять отдельные данные профилей методов из обучающего запуска и собирать их в архив CDS для использования в рабочем запуске. Рабочий запуск тоже добавляет собственную информацию профилирования, и компилятор VM будет использовать самую «свежую» доступную информацию профилей.

Ещё один вид AOT-обработки (в будущем) — сохранение профилей скомпилированного кода с (будущим) флагом -XX:+AOTMethodCompilation. Он будет компилировать методы, которые оказались горячими во время обучающего запуска, и собирать их в архив CDS. VM загружает их по мере необходимости, чтобы ускорить запуск или прогрев. Рабочий запуск тоже добавляет собственные JIT-скомпилированные методы, и VM будет выполнять самые «свежие» доступные методы.

Согласованность обучающего и рабочего запусков

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

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

Чтобы содержимое архива CDS было актуальным, CDS применяет правила, обеспечивающие согласованность обучающих и рабочих запусков. Коротко говоря, CDS гарантирует, что оба запуска в реальном смысле обрабатывают одно и то же приложение. По сути, эти правила и определяют, что значит для двух запусков приложения быть «одинаковыми».

Вот правила согласованности, которые применяет CDS:

  • Оба запуска должны использовать один и тот же выпуск JDK.

  • Оба запуска должны использовать одно и то же семейство ISA процессора, например x86 или ARM (на определённом стандартном уровне). В качестве спекулятивной оптимизации CDS может предполагать точно такое же оборудование (включая специализированные расширения ISA), но предусматривать откат к более общему процессору (с меньшим числом расширений ISA).

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

  • Оба запуска должны использовать одинаковые размер и кодировку управляемых ссылок. (Может ли различаться базовый адрес кучи или коэффициент масштабирования compressed oops, зависит от платформы.) Если эта кодировка задаётся автоматически (эргономической логикой), VM попытается согласовать настройку в рабочем запуске и отклонит архив CDS, если это не удастся.

  • Оба запуска должны использовать согласованные class path. Рабочий запуск может задавать дополнительные элементы class path, добавленные в конец; в остальном class path должны быть идентичны.

  • У обоих запусков должны быть одинаковый граф модулей и одинаковые параметры модульной системы. Если используются параметры -m или --module, их использование должно быть согласованным.

В некоторых случаях обучающий запуск откажется создавать архив CDS, если нет возможности запустить в рабочей среде «то же приложение». Вот эти случаи:

  • Некоторые возможности модульной системы CDS не поддерживает. Это --limit-modules, --patch-module и --upgrade-module-path.

  • CDS поддерживает только class path на основе JAR. Class path на основе каталогов нельзя проверить на согласованность, поскольку содержимое каталогов может меняться одновременно с выполнением приложения.

  • ZGC не поддерживается в рабочих запусках CDS. Но см. https://bugs.openjdk.org/browse/JDK-8326035.

Неподдерживаемые возможности могут получить поддержку в будущем. Требования к согласованности могут быть ослаблены в будущем.

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

Если VM определяет, что не может использовать архив CDS, она будет работать без него (если задан -Xshare:auto) или выдаст диагностическое сообщение об ошибке (если задан -Xshare:on).

CDS допускает многие различия между обучающим и рабочим запусками:

  • Каждый запуск может использовать другую реализацию CPU в пределах того же семейства процессоров.

  • Каждый запуск может запрашивать другой GC, если CDS его поддерживает. Если рабочий запуск запрашивает неподдерживаемый GC, VM может отказаться использовать архив или просто проигнорировать графы объектов, сохранённые в архиве, что ограничит некоторые оптимизации.

  • Запуски могут указывать в командной строке разные главные классы или иным образом проводить время в разных частях кодовой базы. CDS принесёт пользу рабочему запуску лишь в той мере, в какой он повторно использует решения о загрузке и связывании, принятые в обучающем запуске, но VM всё равно будет работать корректно, даже если рабочий запуск полностью «отойдёт от сценария».

  • У двух запусков могут быть разные настройки окружения, например переменные окружения Unix или свойства Java. Если какая-то настройка окружения значима внутри JDK и она различается в обучающем и рабочем запусках, то VM и код JDK решают, какую настройку соблюдать и не отказаться ли от архива CDS полностью.

  • Свойство java.lang.Integer.IntegerCache.high значимо внутри JDK. Оно настраивает кэш объектов с сохраняемой Identity (идентичность объекта), относящийся к автоупаковке int. Сейчас в рабочем запуске CDS учитывает большее из двух значений.

Некоторые оптимизации CDS, например предоставление интернированных строк или связывание байт-кодов invokedynamic, реализованы с помощью архивированных объектов кучи Java. Поэтому эти оптимизации будут недоступны для сборщиков мусора, которые не поддерживают архивированные объекты кучи Java (например, ZGC). Однако большинство оптимизаций CDS, например AOT-загрузка классов и AOT-связывание ссылок на классы, поля и методы, доступны независимо от выбранного сборщика.

Дополнительные ограничения

Вот некоторые дополнительные практические оговорки и ограничения на использование CDS помимо базового требования согласованности обучающих и рабочих запусков:

  • Класс, загруженный в AOT-порядке, остаётся в VM, даже если приложение (в результате своего динамического поведения) фактически не запрашивает загрузку этого класса. Такой класс не подлежит выгрузке классов. Поэтому он занимает память, чего бы не было, если бы он загружался «точно в срок».

  • Пользовательские загрузчики классов не участвуют в AOT-загрузке. Причина в том, что сейчас нет способа отслеживать Identity пользовательского загрузчика классов между обучающим и рабочим запусками. В результате этого ограничения такие классы загружаются «точно в срок» и работают медленнее. Считается, что текущая работа создаёт основу, необходимую для преодоления этого ограничения: сначала стабилизируются классы, которые определяют пользовательские загрузчики классов.

  • Существует ряд граничных случаев, когда классы нельзя загрузить заранее. (Это могут быть классы с пользовательским загрузчиком классов, подписанные классы и классы, использующие очень старую версию верификатора.) Класс, который сам может быть загружен в AOT-порядке, может быть не полностью связываемым в AOT-порядке с другим классом, который нельзя загрузить в AOT-порядке. Иногда CDS может решить отложить загрузку класса просто из-за ограничения по памяти. Во всех таких случаях безопасно вернуться к загрузке и связыванию «точно в срок». Такие ограничения могут быть устранены в будущем, если и когда они окажутся существенными.

  • Определение класса с помощью MethodHandles.Lookup::defineClass() — необратимое решение, если класс именованный. Такие вызовы приведут к LinkageError с сообщением о попытке повторного определения класса, если затронутый именованный класс также был загружен в AOT-порядке из архива CDS. Это стандартная реакция на попытку дважды определить класс с одним и тем же именем.

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

Выбор обучающего запуска

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

Вот несколько конкретных советов:

  • В любой момент рабочего запуска CDS даёт выигрыш только от такого обучающего запуска, который тоже «дошёл до этого места». Рабочий запуск будет «ожидать» только тех событий, которые действительно произошли во время обучающего запуска.

  • Рассмотрите возможность вынести логику управления обучающим запуском в отдельный главный класс, MyAppTraining. Это может быть обёртка вокруг главного класса рабочего запуска, которая позаботится о том, чтобы задействовать все основные режимы и подкоманды приложения.

  • Для оптимизации времени запуска набор классов, загружаемых во время обучения, должен в основном состоять из классов, которые обязательно загружаются при запуске в рабочем режиме. Чтобы наблюдать за загрузкой классов и проверять, что обучающий запуск загружает нужные классы, может пригодиться -verbose:class или другая опция журналирования.

  • Обучающему запуску следует избегать загрузки множества классов, не используемых рабочим запуском, поскольку эти неиспользуемые классы увеличивают объём памяти, занимаемый CDS. Это значит, что очень большие наборы тестов могут добавлять к CDS накладные расходы по памяти. В дальнейшем, возможно, будут изучены способы извлекать пользу из таких классов, но отфильтровывать их из файла архива CDS.

  • При обучении для запуска (а не для полного прогрева) сосредоточьтесь на выполнении широкого набора коротких проверочных сценариев (их также называют «дымовыми тестами» или «проверками работоспособности»). Часто этой работы достаточно, чтобы загрузить все нужные вам классы. Избегайте очень больших наборов тестов, которые охватывают редкие граничные случаи, редко используемую функциональность, нагрузочное или регрессионное тестирование: они не помогают охарактеризовать действия при запуске.

  • В качестве обходного пути, чтобы исключить неиспользуемые тестовые классы, опытный пользователь может вмешаться в фазу компоновки (assembly phase) — вторую часть исходного процесса работы с CDS, на которой архив создаётся с помощью -Xshare:dump. В этой командной строке можно отредактировать путь к классам (class path), чтобы исключить классы (например, тестовые драйверы), используемые только обучающим запуском. Из-за этого в архиве CDS могут появиться неразрешённые ссылки на отсутствующие тестовые классы, которые рабочему запуску никогда не понадобится разрешать. Однако этот обходной путь может приводить к проблемам со стабильностью, поскольку CDS не ожидает встретить «дыры» в пути к классам. В дальнейшем, возможно, появится более удобный способ вручную исключать такие классы.

  • Если приложение получает внешние входные данные от клиентов по сети или от пользователей через графический интерфейс, для задействования классов, обрабатывающих такие данные, может понадобиться имитационная рабочая нагрузка. Если такие классы не попадут в CDS, во время рабочего запуска они будут загружаться из пути к классам или графа модулей по мере необходимости (just-in-time). В дальнейшем, возможно, будут рассмотрены способы вручную добавлять такие классы, если они известны, хотя полноценной замены простому запуску приложения нет.

  • При обучении для прогрева (а не только для запуска) обучающий запуск должен длиться достаточно долго, чтобы VM успела скомпилировать оптимизированный код для сохранения в CDS. (Это предполагает возможность AOT-компиляции методов, которая сейчас существует в виде прототипа и ещё не выпущена.) Чем дольше обучающий запуск проходит горячие пути кода, тем больше оптимизированного кода будет сгенерировано для использования в рабочем режиме.

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

  • В дальнейшем, возможно, появятся удобные «хуки» для ограничения рамок и настройки обучающего запуска. Фаза компоновки может запускаться после заданного объёма загрузки классов, JIT-компиляции или выполнения указанных методов.

Измерение запуска и прогрева

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

Если подробнее, запуск — это последовательность однократных задач настройки, а прогрев — непрерывная оптимизация. Во время запуска VM и приложение загружают, связывают и инициализируют классы, а также настраивают другие ресурсы, например Java-объекты. Приложение прогревается со временем: сначала VM выборочно компилирует байт-код из class-файлов в машинный код, а затем отслеживает «горячие точки» (hot spots) в коде приложения и заново оптимизирует их машинный код. Помимо генерации кода, во время прогрева VM подстраивает некоторые эргономические настройки.

Прогрев и запуск пересекаются в первые миллисекунды после старта приложения. И оба процесса могут растягиваться на неопределённое время: приложение может работать секунды или минуты и внезапно выполнить новые действия запуска, потому что оно приняло запрос нового вида. VM также может долго оптимизировать приложение и в конце концов (через секунды или минуты) достичь устойчивого состояния с пиковой производительностью. Но даже тогда, если внезапно поступит запрос нового вида, VM может понадобиться снова вернуться к прогреву, чтобы учесть новые пути кода. И задачи запуска, и задачи прогрева можно решать техниками AOT или JIT, спекулятивными или нет, и обычно всеми перечисленными сразу. Таким образом, запуск и прогрев — разные наборы действий, и каждый из них заслуживает отдельного внимания при оценке и улучшении технологий VM.

В общей картине запуск и прогрев — не единственные важные показатели качества. Выполняя свои задачи, приложение должно расходовать умеренное количество времени и памяти, обеспечивая хорошие показатели пропускной способности (время на единицу рабочей нагрузки) и объёма занимаемой памяти (размер рабочей памяти). Разумеется, оно также должно быть корректным (выдавать правильные ответы) и стабильным (выполняться предсказуемо, без сбоев и любого другого некорректного поведения). Пропускная способность, корректность и стабильность всегда были ключевыми ценностями экосистемы Java. Проект Leyden заново сосредоточен на улучшении запуска, прогрева и объёма занимаемой памяти за счёт переноса выбранных вычислений на новые моменты времени: либо раньше (Ahead-of-Time, AOT), либо позже (just in time, JIT). В этой общей картине данная работа посвящена AOT-оптимизациям для улучшения запуска, а в будущем и прогрева.

Для каждого развёрнутого приложения потребуется собственное определение того, что составляет одно повторение его повторяемой рабочей нагрузки: это может быть запрос к сервису, интеграционный тест, бенчмарк, нагрузочный тест или какой-то другой «сводный тест» многих частей приложения. Первое повторение загружает и инициализирует все нужные классы и структуры данных приложения, а последующие повторения побуждают VM оптимизировать приложение, в итоге достигая пиковой производительности. Для такого приложения и его повторяемой рабочей нагрузки прогрев можно измерять как время достижения заданной доли (например, 95 %) итоговой пиковой пропускной способности, а запуск — как время, за которое первое повторение нагрузки доходит до некоторой специфичной для приложения «точки готовности» или же до конца первого повторения нагрузки.

Краткая история CDS

В том или ином виде CDS встроен в HotSpot VM начиная с JDK 5, то есть с 2004 года. Изначально аббревиатура расшифровывалась как «Class Data Sharing» (совместное использование данных классов), что отражено в опциях вроде -Xshare:…. С годами возможности CDS вышли за рамки хранения разделяемых данных классов, поэтому теперь предпочтительна альтернативная расшифровка CDS — «Cache Data Store» (хранилище кэшированных данных). Обе фразы обозначают одну и ту же технологию.

Начиная с 2017 года каждая среда выполнения Java содержит AOT Cache (кэш, подготовленный заранее) из более чем 1000 определений основных классов JDK, который создаётся при сборке JDK из исходного кода. В этом смысле CDS присутствует повсюду, даже у Java-программистов, которые никогда о нём не слышали. И всё же на протяжении большей части своего существования CDS был возможностью скорее для «опытных пользователей».

FIXME (Здесь — подробнее о появлении основных возможностей CDS, таких как динамический архив или кэшированные объекты.)

CDS и совместное использование

CDS использует отображение в память (memory mapping), чтобы быстро заполнить память VM всем содержимым файла архива CDS, после чего VM может выбирать ресурсы из файла для принятия в свои действующие метаданные. Это отображение перемещаемое, но организовано так, чтобы по возможности использовать определённый базовый адрес. Если это удаётся, страницы отображённого файла не нужно редактировать для перемещения встроенных в них указателей (и тем самым делать их «грязными» из-за копирования при записи). Чистые страницы позволяют разделять отображения между процессами VM, что сокращает объём занимаемой памяти. Именно это поведение послужило основанием для (ныне устаревшей) расшифровки аббревиатуры «Class Data Sharing».

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

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

В дальнейшем объём занимаемой памяти, вероятно, будет сокращён сочетанием нескольких мер: «возвращением» совместного использования, потерянного из-за ASLR, дальнейшей настройкой компромисса между избыточным и недостаточным обеспечением ресурсами и офлайн-сжатием редко используемых ресурсов (обменивая время на место).

Глоссарий

Ниже приведён список терминов, полезных при обсуждении CDS.

  • принятие (adoption) — решение VM во время рабочего запуска использовать определённый ресурс CDS (не называется «загрузкой» или «связыванием», потому что это вносило бы путаницу)
  • Ahead-of-Time (или AOT) — прилагательное, описывающее порядок некоторого оптимизируемого действия как каким-либо образом подготовленного заранее
  • фаза компоновки (assembly phase) — особая операция VM, которая объединяет информацию, собранную во время обучающего запуска, компонует её в образ, пригодный для отображения в память, и записывает его в файл архива CDS
  • Cache Data Store (или CDS) — кэш, хранящий данные VM специально для улучшения запуска, прогрева или объёма занимаемой памяти приложения
  • архив CDS — историческое название файла CDS
  • ресурс CDS (CDS asset) — блок данных в файле CDS, кодирующий некоторое конкретное решение или группу связанных решений (например, загруженный класс, связанный элемент пула констант, профиль метода, скомпилированный метод, кэшированный рефлективный объект и т. д.)
  • файл CDS — файл, содержащий данные, из которых состоит Cache Data Store; данные организованы как пригодная для отображения в память коллекция логически независимых ресурсов
  • скомпилированный код (или «nmethod») — сущность в кэше кода VM, содержащая оптимизированный машинный код некоторого метода; может быть (гипотетическим) ресурсом CDS
  • just in time (или JIT) — прилагательное, описывающее порядок некоторого оптимизируемого действия как выполняемого лениво или по требованию (в отличие от существительного JIT, обозначающего JIT-компилятор)
  • связанная константа (linked constant) — ячейка в табличной сущности метаданных (а именно в пуле констант), с помощью которой один класс (владелец пула констант) может использовать другой класс, метод или поле; может быть ресурсом CDS
  • загруженный класс (loaded class) — сущность метаданных, представляющая загруженный класс или интерфейс; может быть ресурсом CDS (предварительно разобранным или «живым»)
  • профиль метода (method profile) — сущность метаданных, содержащая сведения о прошлых (и вероятных будущих) выполнениях метода; может быть (гипотетическим) ресурсом CDS
  • перемещаемый сегмент (relocatable segment) — одна из нескольких больших областей файла CDS, которая отображается в память VM как единое целое; ресурсы в одном сегменте всегда находятся на фиксированном относительном смещении друг от друга
  • перемещение (relocation) — то, как образ указателя в отображённом файле CDS становится «живым» указателем в VM
  • карта перемещений (relocation map) — битовая карта в архиве CDS, которая указывает расположение указателей и тем самым управляет процессом перемещения
  • запуск, рабочий (run, production) — запуск приложения, в котором используется файл CDS
  • запуск, обучающий (run, training) — запуск приложения, который создаёт файл CDS
  • сценарий работы, с автообучением (workflow, auto-training) — нестандартный сценарий работы, в котором запуски приложения автоматически и прозрачно для пользователя классифицируются как обучающие или рабочие
  • сценарий работы, нестандартный (workflow, non-standard) — (гипотетическая) серия взаимосвязанных запусков приложения, которые создают и/или используют данные CDS, но не являются стандартным сценарием работы, поддерживаемым CDS
  • сценарий работы, стандартный (workflow, standard) — обучающий запуск, архив которого затем используется набором рабочих запусков