Как уже говорилось в предыдущих статьях, в JVM HotSpot имеется два JIT-компилятора: C1 и C2. Первый компилятор — клиентский (client), который быстренько компилирует код при старте и первых минутах работы JVM, потом некоторое время собирает статистику по работе приложения, после чего в дело вступает компилятор C2 — серверный (server) — который делает более глубокие оптимизации и создает более быстрый машинный код.
Компилятор C2 — надежная рабочая лошадка
Компилятор C2 замечательно себя зарекомендовал за столько лет. Он прекрасно справляется со своей работой. Но пришло время признать, что время жизни компилятора C2 подошло к концу:
- C2 компилятор написан на C++. И это создает определенные проблемы для дальнейшего развития. Его трудно писать, изменять и поддерживать: малейшая ошибка в коде может привести к краху JVM и важного приложения, а хороших C++ программистов не так уж много и они дорого стоят.
- Больше улучшать в нем уже нечего, ресурс улучшений исчерпан. За последнее время в компилятор C2 не вносилось никаких существенных изменений, а дальнейшие улучшения в C2 компиляторе планируются только за счет использования новых дополнительных instrinsics.
Какой был предложен выход
Первое, было предложено открыть API компилятора в JVM так, чтобы он был доступен извне и вместо одного компилятора можно было бы подключать к JVM любой другой. Начиная с Java 9 в JVM этот новый API — JVMCI (JVM Compiler Interface) — стал доступен в качестве экспериментальной опции, которая включается так:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI
Написание собственного компилятора — нетривиальная задача. Но в JVM она упрощается тем фактом, что код из исходников уже скомпилирован в байт-код JVM. Этот байт-код JVM интерпретирует на лету и выполняет его до тех пор, пока не вступит в силу JIT-компилятор, задача которого взять байт-код и преобразовать его в набор машинных инструкций. То есть на входе компилятор получает массив байтов (byte[] in) и выдает на выходе тоже набор байтов (byte[] out).
В теории нет никаких причин, по которым это преобразование одного массива байтов в другой должно непременно быть написано на С++. Его можно написать и на Java! И такой компилятор не будет работать, как нечто инородное, а как набор потоков, работающих параллельно с потоками самого приложения. Такой компилятор, конечно, будет работать медленнее чем, написанный на C++, но ведь код компилятора тоже может быть соптимизирован, то есть компилятор может компилировать сам себя. И работать после такой компиляции он будет также быстро, как если б он был изначально написан на С++!
Начиная с Java 10 JVM HotSpot поставляется с двумя встроенными JIT-компиляторами: C2, который включается по умолчанию, и новый JIT-компилятор graal, который включен в JVM как экспериментальный (JEP 317) и написан на Java.
Внимание: в JDK 17 компилятор graal отсутствует! Читайте Update в конце статьи.
Как включить graal
Новый экспериментальный JIT-компилятор graal включается с помощью следующих флагов:
-XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI -XX:+UseJVMCICompiler
-XX:+UnlockExperimentalVMOptions — открывает доступ к экспериментальным опциям
-XX:+EnableJVMCI — открывает доступ к интерфейсу компилятора
-XX:+UseJVMCICompiler — включает внутренний экспериментальный JIT-компилятор graal
Если JVM запущена с флагом -XX:+UseJVMCICompiler, но без флага -XX:+EnableJVMCI, будет использован JIT-компилятор graal, но не будет доступа к API.
Если -XX:+EnableJVMCI, а -XX:+UseJVMCICompiler отсутствует, JVM будет использовать C2 JIT-компилятор.
Короче, для запуска JVM с JIT-компилятором graal как минимум ОДИН флаг -XX:+UseJVMCICompiler, а для полного доступа к API — ДВА флага: -XX:+EnableJVMCI и -XX:+UseJVMCICompiler.
Как увидеть, какой компилятор работает
Чтобы увидеть, как происходит компиляция, добавьте параметр -XX:+PrintCompilation. В консоли вывода вы увидите следующие строки:
82 1 3 java.lang.String::charAt (25 bytes)
83 3 3 java.lang.String::isLatin1 (19 bytes)
83 2 3 java.lang.StringLatin1::charAt (28 bytes)
84 5 3 java.lang.Object::<init> (1 bytes)
84 6 3 java.lang.StringLatin1::hashCode (42 bytes)
84 4 3 java.lang.String::hashCode (60 bytes)
Если же включить graal, то среди строк компиляции вы увидите следующее:
1310 3958 % 1 org.graalvm.compiler.phases.schedule.SchedulePhase$Instance$GuardOrder::propagatePriority @ 53 (307 bytes)
1312 4013 % 1 org.graalvm.compiler.loop.LoopFragment::computeNodes @ 396 (653 bytes)
1316 3836 1 org.graalvm.compiler.nodes.cfg.Block::getKillLocationsBetweenThisAndDominator (179 bytes)
1317 3761 % 1 org.graalvm.compiler.lir.alloc.lsra.LinearScanLifetimeAnalysisPhase::numberInstructions @ 145 (309 bytes)
Строки org.graalvm.compiler … говорят то том, что graal включен и он компилирует сам себя.
Как подключить другой JIT-компилятор
Как было сказано выше, API JVMCI позволяет подключить и внешний JIT-компилятор, который написал кто-то другой или вы сами. Для этого предусмотрен дополнительный флаг -Djvmci.Compiler=<compiler-name>. Например,
-Djvmci.Compiler=graal
требует от JVM включить компилятор graal, который и так включается по-умолчанию. Если в вашем распоряжении имеется свой компилятор на замену graal-ю, укажите его название в этом флаге, скажем:
-Djvmci.Compiler=mySuperPuperJITCompiler
JMVC API позволяет управлять более тонко работой компилятора с помощью определенных флагов:
bool BootstrapJVMCI = false
bool EagerJVMCI = false
bool EnableJVMCI = true
bool EnableJVMCIProduct = false
intx JVMCICounterSize = 0
bool JVMCICountersExcludeCompiler = true
intx JVMCIHostThreads = 1
ccstr JVMCILibDumpJNIConfig =
ccstr JVMCILibPath =
intx JVMCINMethodSizeLimit = 655360
bool JVMCIPrintProperties = false
intx JVMCIThreads = 1
intx JVMCITraceLevel = 0
bool PrintBootstrap = true
bool UseJVMCICompiler = true
bool UseJVMCINativeLibrary = false
Например, флаг -XX:JVMCIThreads=<n> позволяет задать количество потоков, в которых будет работать компилятор. Флаг -XX:+JVMCIPrintProperties позволяет вывести все свойства компилятора graal, которыми тоже можно управлять. И прочее.
Выводы
JIT-компилятор graal по-прежнему (даже в свежем JDK 15) является экспериментальным. По-прежнему компилятор C2 является вашим надежным оружием в достижении low-latency. Но не забывайте, что за graal — будущее и с каждым новым релизом JDK это будущее приближается. JVMCI API позволяет подключить свой компилятор и настроить компиляцию его под свои требования к low-latency намного лучше, чем это позволяет C2.
Компилятор graal не следует путать с проектом GraalVM, который охватывает несколько технологий: 1) технологию JIT-компилятора (graal), 2) технологию поддержки множества языков на VM и 3) runtime для этого.
Update: В сентябре 2022 года вышла версия Java 17. В ней был реализован JEP 410, который удаляет компилятор graal из JDK 17. Не понимаю, почему столь интересная и полезная функция была удалена, и никто не протестовал против этого. Интерфейс JVMCI оставлен и может быть использован по-прежнему для подключения стороннего компилятора.