Новый перевод от команды Spring АйО расскажет вам о разных уровнях JIT (Just in Time) компиляции, о преимуществах такого подхода к компиляции Java приложений по сравнению с традиционным способом, а также покажет на примерах, что происходит при компиляции приложения и какой ассемблерный и машинный код будет сгенерирован при использовании разных опций JIT компиляции.
Я предполагаю, что вы уже клонировали и собрали JDK.
В первой части мы посмотрим на:
Запуск простого примера на Java.
Компиляцию в Java bytecode с помощью
javac
.Различие между релизными и debug билдами.
Почему JIT компиляция?
Многоуровневую компиляцию.
Посмотрим на C2 IR и сгенерированные ассемблерные инструкции.
Комментарий от редакции Spring АйО
Речь идет о C2 Intermediate Representation (он же IR). В теории компиляторов IR является временным промежуточным представлением кода. Известный пример — IR, который генерирует LLVM. Соответственно, этот IR затем преобразуется в исполняемые машинные инструкции. Подобная схема характерна и для C2-компилятора.
Наш первый пример
Мы начнем с очень простого примера Test.java
:
public class Test {
public static void main(String[] args) {
// Especially with a debug build, the JVM startup can take a while,
// so it can take a while until our code is executed.
System.out.println("Run");
// Repeatedly call the test method, so that it can become hot and
// get JIT compiled.
for (int i = 0; i < 10_000; i++) {
test(i, i + 1);
}
System.out.println("Done");
}
// The test method we will focus on.
public static int test(int a, int b) {
return a + b;
}
}
Мы можем запустить его вот так:
$ java Test.java
Run
Done
Компиляция исходного кода на Java в Java bytecode
Java (и другие языки программирования, использующие JVM) сначала компилируются в Java Bytecode. Этот bytecode все еще нейтрален к платформе (в том плане, что он независим от архитектуры CPU и OS) и должен исполняться в JVM. JVM может интерпретировать bytecode или скомпилировать его далее в привязанный к платформе машинный код.
Мы можем в явном виде скомпилировать наш тестовый файл в bytecode:
$ javac Test.java
Таким образом будет сгенерирован файл Test.class
. Мы можем проинспектировать его содержимое:
$ javap -c Test.class
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #13 // String Run
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: iconst_0
9: istore_1
10: iload_1
11: sipush 10000
14: if_icmpge 31
17: iload_1
18: iload_1
19: iconst_1
20: iadd
21: invokestatic #21 // Method test:(II)I
24: pop
25: iinc 1, 1
28: goto 10
31: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
34: ldc #27 // String Done
36: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
39: return
public static int test(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
}
На этом этапе вам необязательно понимать этот bytecode до мельчайших деталей. На высоком уровне мы видим, что в классе Test
присутствуют три метода:
<init>
: это код конструктора по умолчанию, который вызывает конструктор суперклассаObject
. Отметим, что javac автоматически добавил конструктор по умолчанию за нас, несмотря на то, что мы не задавали его в явном виде.main
: мы видим несколькоinvokevirtual
дляprintln
, одинinvokestatic
дляtest
иgoto 10
инструкцию (она реализует циклfor
) в bytecode-е, находящуюся на оффсете28
для backedge цикла.test
: два аргумента типаint
помещаются по локальным адресам0
и1
.iload_0
иiload_1
берут эти аргументы с их локальных адресов и помещают их на вершину stack-а.iadd
убирает два значения типаint
с вершины стека, суммирует их, и кладет на вершину стека результат суммы. Далееireturn
инструкция забирает эту сумму с вершины стека и возвращает ее в вызываемый метод.
Если вы хотите узнать больше о Java Bytecode:
Java Bytecode (Wikipedia): полезный справочник, содержащий все существующие JVM bytecode инструкции.
Asmtools: инструмент для ассемблирования / дизассемблирования класс-файлов. Когда я работаю над багом, и у меня есть только класс-файл, я часто инспектирую его с помощью
jdis
, модифицирую его и снова компилирую с помощьюjasm
. Так у меня часто получается рано или поздно получить примерное представление об исходном Java коде.
Примечание 1: Когда мы напрямую исполняем Test.java
, JVM неявно сначала компилирует файл в bytecode и затем напрямую выполняет класс-файл. Таким образом это работает только начиная с JDK 11.
Примечание 2: .jar
файлы — это просто zip-каталоги различных .class
файлов.
Типы сборок JDK
Три наиболее часто встречающихся в моей повседневной работе типа сборок следующие:
Релизная: быстрая, но отлаживать ее труднее. Эта сборка предназначена для пользователей Java. Она не выполняет никакие assert инструкции в С++ коде самой HotSpot VM. Мы регулярно добавляем дополнительный проверочный код в форме assertion-инструкций, когда меняем или добавляем новый код VM. Этот код помогает раньше заметить проблему. Но он может серьезно замедлить выполнение программы на Java. Поэтому мы не включаем такой код в релизные сборки JDK, чтобы достичь максимальной производительности.
Комментарий от редакции Spring АйО
Речь идёт о том, что разработчики рантаймов, в частности разработчики HotSpot JVM, добавляют в реализацию самой виртуальной машины большое количество assert-выражений (HotSpot JVM написана на C++, поэтому автор ссылается на инструкции assert в C++). Эти выражения необходимы для проверки того, что все необходимые инварианты при работе того или иного механизма внутри JVM соблюдаются.
Отладочная (быстрая) или fastdebug: работает медленнее релизной, но выполняет инструкции assert и учитывает флаги VM, предназначенные для дебага.
Отладочная (медленная) или slowdebug: работает медленнее, чем быстрая отладочная сборка. Компилятор C++ работает с меньшим количеством оптимизаций, что влияет на производительность финальной сборки, но при этом у VM появляется больше символов, все переменные доступны, инлайнинги отсутствуют и т.д. , что дает лучший отладочный опыт с GDB / RR.
Комментарий от редакции Spring АйО
Под символами подразумеваются определённые конструкции в объектных файлах, которые видны линковщику (linker, иногда в русскоязычном сегменте его называют «компоновщик»). Дело в том, что компиляторы, особенно для нативных языков, достаточно умны. Они отлично справляются с инлайнингом, развёрткой циклов и другими оптимизациями. Из-за таких оптимизаций, например, инлайнинга, компилятор C++ не сформирует в объектных файлах те «символы», которые соответствуют исходному коду, что затрудняет отладку. Поэтому в индустрии в целом (без привязки к Java) в дебажных сборках обычно отключают все или определённые оптимизации компиляторов.
Различия в этих сборках состоит в основном в разных уровнях GCC оптимизации. Отладочные сборки содержат больше отладочных флагов. И только отладочные сборки выполняют инструкции assert (то есть дополнительные проверки).
Я предпочитаю работать с fastdebug по умолчанию, но переключаюсь на slowdebug, если GDB / RR ведут себя неожиданным образом (например, не прерываются на той строке, где должны).
Комментарий от редакции Spring АйО
Эти дебаггеры, как правило, не встречаются в мире Java, поэтому можно не уделять им особого внимания. Но всё же поясним:
GDB — GNU DeBugger. RR — ещё один дебаггер, который, вероятно, не очень известен в Java-сообществе. Он построен на основе GDB и предоставляет дополнительный функционал.
Их чаще всего используют для отладки кода на C/C++.
Вывод лога компиляции при помощи VM CompileCommand
Теперь давайте продолжим работать с нашим примером и посмотрим, какие методы скомпилированы. Это можно сделать, используя специальный флаг CompileCommand
, который задает различные дополнительные опции управления. Одна из них — это printcompilation
, которая выводит на экран лог скомпилированных методов (чтобы получить более подробную информацию о CompileCommand, вы можете воспользоваться командой java -XX:CompileCommand=help --version
).
$ java -XX:CompileCommand=printcompilation,*::* Test.java
CompileCommand: PrintCompilation *.* bool PrintCompilation = true
360 1 3 java.lang.Byte::toUnsignedInt (6 bytes)
363 2 3 java.lang.Object::<init> (1 bytes)
390 3 4 java.lang.Byte::toUnsignedInt (6 bytes)
...
it continues for a while, and ends with
...
10874 2000 3 java.lang.invoke.InvokerBytecodeGenerator::isStaticallyInvocable (168 bytes)
10874 2005 3 sun.nio.fs.UnixPath::getPathForExceptionMessage (5 bytes)
Run
10876 2007 2 Test::test (4 bytes)
Done
10881 2006 3 sun.nio.fs.UnixException::translateToIOException (133 bytes)
Из этого лога му уже можем узнать довольно много от том, как HotSpot компилирует и выполняет наш код:
Здесь довольно много компиляций, которые происходят не из нашего
Test.java
. Они инициируются Java runtime и библиотеками, например, во время запуска JVM до момента, когда вызывается наш методmain()
вTest.java
.Первая колонка отображает время, когда происходит компиляция, в миллисекундах.
Test::test
выполняется через 10876 миллисекунд.Вторая колонка является уникальным идентификатором компиляции, который, как мы видим, растет инкрементально. Отметим, что эти идентификаторы не всегда идут в идеальном порядке, по причине компиляции методов несколькими потоками компилятора, которые работают параллельно, при этом каждая компиляция может занимать разное время.
Третья колонка показывает, какой компилятор используется. Уровни 1-3 используются для C1, уровень 4 — для C2.
Четвертая колонка показывает имя скомпилированного метода и размер его bytecode.
Обычно нас интересует только компиляция определенных классов. Мы можем ограничить вывод лога некоторыми классами, например нашим классом Test
. Этого можно достичь модификацией команды printcompilation
следующим образом:
$ java -XX:CompileCommand=printcompilation,Test::* Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
Run
10405 1983 3 Test::test (4 bytes)
Done
Зачем использовать Just In Time (JIT) компилятор?
Компилятор типа Ahead Of Time (AOT) компилирует код один раз и выдает исполняемый файл. При использовании GCC вы компилируете ваш C код один раз и отдаёте пользователям исполняемый файл. Этот исполняемый файл может работать только на тех платформах, под которые он был скомпилирован. Вы не можете выполнять файл, скомпилированный под x64, на машине aarch64. Если вы использовали ассемблерные инструкции AVX512 в исполняемом файле, вы сможете запустить его только на тех машинах, которые поддерживают эти инструкции.
Комментарий от редакции Spring АйО
AVX512 — это относительно новый набор расширений для архитектуры x86, предложенный Intel. Его поддерживают большинство современных x86-чипов Intel и AMD, и он используется для различных целей. Типичный пример использования — операции над векторами.
Компилятор типа (JIT) компилирует код в рантайме. Это приводит к целому набору как дополнительных вызовов, так и дополнительных возможностей:
Компиляция происходит параллельно с выполнением. Поток интерпретации байткода и его компиляции конкурируют между собой за ресурсы. Таким образом JIT компиляторы имеют сильный стимул для более быстрой компиляции, чтобы минимально задерживать работу приложения.
В момент запуска никакой код еще не скомпилирован. Любой исполняемый код работает в режиме интерпретации, что довольно медленно. Так как компиляция “Горячих” фрагментов кода занимает время, перформанс приложения также растет не быстро после старта.
Вместо того, чтобы распространять исполняемые файлы, которые зависят от платформы, можно распространять платформонезависимый исходный код (Java код или Java bytecode). JIT компилятор знает все о платформе, на которой работает, и может сгенерировать код, оптимизированный именно под эту архитектуру CPU.
Динамическая загрузка нового кода: JVM позволяет новым классам загружаться в рантайме, с возможностью вызывать их методы. AOT компилятор ничего не знал бы о динамических классах времени компиляции. JIT компилятор позволяет новому коду компилироваться в рантайме, таким образом улучшая пропускную способность при выполнении динамического кода.
Новые возможности оптимизации: мы можем делать предположения во время компиляции, основанные на гипотезах, что, возможно, позволит генерировать более быстрый код. Если такое гипотетическое предположение опровергается в рантайме, например, проверка данной гипотезы не проходит, мы всегда можем откатить оптимизацию и вернуться обратно к интерпретатору. Вот несколько примеров:
Если интерфейс имеет только одну реализацию, мы можем использовать статические вызовы вместо динамических (то есть виртуальных) вызовов. Если мы когда-либо загрузим вторую реализацию того же интерфейса, можно будет перекомпилировать с использованием динамических вызовов (dynamic dispatch).
Мы можем профилировать выполнение в режиме интерпретатора (и также в режиме C1) и использовать эту профильную информацию для управления нашей (C2) компиляцией. Если какая-то ветвь алгоритма никогда не выполняется, мы просто избегаем компиляции этой ветви и проводим деоптимизацию, если переход на нее все же когда-то произойдет. Это снижает время компиляции, потому что мы компилируем меньше кода.
Если профайлер говорит нам, что проверка на null
ни разу не потерпела неудачу, мы можем использовать неявные проверки на null-checks: мы удаляем проверки на null
, и если мы получаем SIGSEGV
в этом месте из-за попытки сослаться на nul
, мы ловим этот сигнал и деоптимизируем, а также выбрасываем NullPointerException
из интерпретатора.
Комментарий от редакции Spring АйО
SIGSEGV — это специальный сигнал операционной системы, который она посылает процессу при обращении к участку памяти, который не существует, либо к участку памяти, к которой у процесса нет доступа.
Многоуровневая компиляции и флаги VM для контроля за компиляцией
В приведенном выше примере выполнения программы мы заметили, что из всего содержимого Test.java
только Test::test
был скомпилирован и только компилятором C1 (уровни 1, 2 или 3). Но почему же main
метод не компилируется?
HotSpot JVM выполняет ваш Java код (bytecode) одним из следующих способов:
Интерпретатор: первоначально весь код выполняется интерпретатором. Это означает, что мы можем начать выполнять код немедленно, но на малой скорости. Мы профилируем какой именно код выполняется, например, посчитав, сколько раз вызывается тот или иной метод. Если мы достигаем определенного порога, мы принимаем решение, что этот метод надо скомпилировать, поэтому мы добавляем этот метод к очереди на компиляцию. Но одновременно с этим мы продолжаем выполнять код в интерпретаторе. Если мы снова попадаем в этот метод, и компиляция уже завершена, мы выполняем скомпилированный код.
C1: как только в процессе профилирования выяснилось, что код является достаточно горячим (например, вызывается достаточно часто), мы компилируем метод с использованием C1. Целью C1 является генерация оптимизированного машинного кода с малым избыточным потреблением ресурсов во время компиляции. Получившийся в результате код уже намного быстрее интерпретируемого кода. Чтобы это заработало, C1 выполняет только очень ограниченную оптимизацию, потому что на этой стадии мы пока не хотим тратить на нее больше времени. C1 также добавляет профилированный код к машинному коду, чтобы мы могли продолжать считать количество вызовов. Если мы замечаем, что код вызывается гораздо чаще, мы рано или поздно можем захотеть сгенерировать более оптимизированный машинный код. Если определенное количество вызовов метода превышено, мы помещаем метод в очередь на компиляцию еще раз, но на этот раз с помощью C2.
C2: как только мы при профилировании определили, что код является очень горячим, мы хотим сгенерировать высокооптимизированный машинный код. Мы согласны платить за это более долгим временем компиляции, поскольку ожидаем, что в будущем этот код будет выполняться много раз. Сокращение общего времени выполнения с более быстрым кодом (в идеале) перевешивает стоимость времени, затраченного на более сложный алгоритм оптимизации во время компиляции с применением C2.
Еще несколько важных моментов:
Информация, полученная при профилировании, используется не только при подсчете вызовов метода для поиска горячего кода, но также, что наиболее важно, для задания верного направления агрессивной/оптимистичной оптимизации, выполняемой компилятором C2. C2 не только более медленный, при его использовании появляется риск, что скомпилированный код будет сразу же деоптимизирован.
Все сказанное — упрощенная картина. Возможны различные пути, то есть иногда мы сразу компилируем на уровне C2 или остаемся на C1, возможны также и разные уровни профилирования.
OSR (On Stack Replacement): если у нас есть цикл, который выполняет очень много итераций, мы можем захотеть скомпилировать его, пока находимся в цикле. Как только мы выходим на backedge и компиляция кода завершается, мы можем входить в скомпилированный код в этой точке.
Комментарий редакции Spring АйО
Под «backedge» автор имеет в виду локальную инструкцию байткода, которая заставляет instruction pointer потока вернуться на определённый оффсет в памяти. Иными словами, это последняя инструкция в цикле, после которой потенциально начинается повторная итерация.
Вернемся к нашему примеру. Мы увидели, что Test::main
не был скомпилирован, то есть он, скорее всего, выполнялся исключительно интерпретатором. Test::test
сначала выполняется интерпретатором, затем оказывается достаточно горячим для C1 компиляции.
Мы можем заставить весь код выполняться в интерпретаторе, используя флаг -Xint
:
$ java -XX:CompileCommand=printcompilation,Test::* -Xint Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
Run
Done
Обычно компиляция происходит в фоновом режиме, что означает, что, когда компиляция метода попадает в очередь, мы продолжаем его выполнение интерпретатором, пока затребованная компиляция не завершится. Это асинхронное поведение может иногда сделать компиляцию немного непредсказуемой. Иногда имеет смысл отключить возможность фоновой компиляции для целей отладки при помощи опции -Xbatch
:
$ java -XX:CompileCommand=printcompilation,Test::* -Xbatch Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
Run
25090 1835 b 3 Test::test (4 bytes)
25090 1836 b 4 Test::test (4 bytes)
Done
Мы видим, что теперь код компилируется сначала C1 и затем C2. Test::test()
был достаточно горячим, чтобы попасть в очередь на C2 компиляцию. Без -Xbatch
выполнение программы успевало полностью завершиться до C2 компиляции метода. При использовании -Xbatch
мы в явном виде дожидаемся завершения всех компиляций прежде чем начать выполнять метод. Мы также видим, что блокирующее поведение сделало выполнение всей программы намного более медленным. Это происходит потому, что VM теперь блокирует выполнение всякий раз, когда требуется произвести компиляцию, причем не только в нашем классе Test
, но и во время запуска JVM.
Иногда может быть полезно ограничить компиляцию только некоторыми классами или методами:
$ java -XX:CompileCommand=printcompilation,Test::* -Xbatch -XX:CompileCommand=compileonly,Test::test Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
Run
7680 85 b 3 Test::test (4 bytes)
7680 86 b 4 Test::test (4 bytes)
Done
Мы также можем принудительно вызвать немедленную компиляцию всех выполняемых методов и пропустить стадию интерпретатора полностью, используя опцию -Xcomp
. В этом случае накладывание ограничений на компиляцию становится еще более важным. Иначе нам придется компилировать все классы и методы, используемые от момента запуска JVM, что может занять много времени.
Мы можем остановить многоуровневую компиляцию на определенном уровне, например избегать любых C2 компиляций и разрешить только C1:
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:TieredStopAtLevel=3 Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
Run
7580 85 b 3 Test::test (4 bytes)
Done
Используя -XX:-TieredCompilation
, мы можем запретить многоуровневую компиляцию, и тогда будет использоваться только C2:
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
Run
8236 85 b Test::test (4 bytes)
Done
Первый взгляд на C2 IR
Большая часть работы компилятора проходит на уровне C2, и лишь незначительная часть на уровне C1. Поэтому мы сосредоточимся на C2 IR.
Используя опцию -XX:+PrintIdeal
, мы можем отобразить машинонезависимое C2 IR (intermediate representation — промежуточное представление), которое иногда называется также “ideal graph” (идеальный граф) или просто “C2 IR”, после того, как большинство оптимизаций уже проведены, но до генерации кода:
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:+PrintIdeal Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
Run
8211 85 b Test::test (4 bytes)
AFTER: print_ideal
0 Root === 0 24 [[ 0 1 3 ]] inner
3 Start === 3 0 [[ 3 5 6 7 8 9 10 11 ]] #{0:control, 1:abIO, 2:memory, 3:rawptr:BotPTR, 4:return_address, 5:int, 6:int}
5 Parm === 3 [[ 24 ]] Control !jvms: Test::test @ bci:-1 (line 17)
6 Parm === 3 [[ 24 ]] I_O !jvms: Test::test @ bci:-1 (line 17)
7 Parm === 3 [[ 24 ]] Memory Memory: @BotPTR *+bot, idx=Bot; !jvms: Test::test @ bci:-1 (line 17)
8 Parm === 3 [[ 24 ]] FramePtr !jvms: Test::test @ bci:-1 (line 17)
9 Parm === 3 [[ 24 ]] ReturnAdr !jvms: Test::test @ bci:-1 (line 17)
10 Parm === 3 [[ 23 ]] Parm0: int !jvms: Test::test @ bci:-1 (line 17)
11 Parm === 3 [[ 23 ]] Parm1: int !jvms: Test::test @ bci:-1 (line 17)
23 AddI === _ 10 11 [[ 24 ]] !jvms: Test::test @ bci:2 (line 17)
24 Return === 5 6 7 8 9 returns 23 [[ 0 ]]
Done
Вот так выглядит промежуточное представление нашего кода на Java из примера:
public static int test(int a, int b) {
return a + b;
}
Давайте посмотрим на это с конца: у нас есть команда return
, которая возвращает результат сложения двух параметров метода a
и b
. Мы можем найти те же операции в IR из вывода -XX:+PrintIdeal: 24 Return
возвращает полученное значение из IR ноды 23 AddI
. 23 AddI
складывает два параметра: 10 Param
и 11 Param
. Другие ноды нас в настоящий момент не интересуют, но мы вернемся к некоторым из них позже.
Мы можем визуализировать вывод -XX:+PrintIdeal
с использованием IGV.
Первый взгляд на сгенерированный ассемблерный код
Используя -XX:CompileCommand=print,Test::test
, мы можем вывести на экран большое количество информации о компиляции. Ниже приведен пример. Мы проигнорируем большую его часть и поговорим только о том, что представляет для нас интерес на данный момент.
Много кода
$ java -XX:CompileCommand=printcompilation,Test::* -XX:CompileCommand=compileonly,Test::test -Xbatch -XX:-TieredCompilation -XX:CompileCommand=print,Test::test Test.java
CompileCommand: PrintCompilation Test.* bool PrintCompilation = true
CompileCommand: compileonly Test.test bool compileonly = true
CompileCommand: print Test.test bool print = true
Run
8254 85 b Test::test (4 bytes)
============================= C2-compiled nmethod ==============================
#r018 rsi : parm 0: int
#r016 rdx : parm 1: int
# -- Old rsp -- Framesize: 32 --
#r623 rsp+28: in_preserve
#r622 rsp+24: return address
#r621 rsp+20: in_preserve
#r620 rsp+16: saved fp register
#r619 rsp+12: pad2, stack alignment
#r618 rsp+ 8: pad2, stack alignment
#r617 rsp+ 4: Fixed slot 1
#r616 rsp+ 0: Fixed slot 0
#
----------------------- MetaData before Compile_id = 85 ------------------------
{method}
- this oop: 0x00007fc87d0943c8
- method holder: 'Test'
- constants: 0x00007fc87d094030 constant pool [36] {0x00007fc87d094030} for 'Test' cache=0x00007fc87d0944d8
- access: 0x9 public static
- flags: 0x4080 queued_for_compilation has_loops_flag_init
- name: 'test'
- signature: '(II)I'
- max stack: 3
- max locals: 2
- size of params: 2
- method size: 14
- vtable index: -2
- i2i entry: 0x00007fc8ac3ecf00
- adapters: AHE@0x00007fc8a8238520: 0xaa i2c: 0x00007fc8ac454380 c2i: 0x00007fc8ac45445e c2iUV: 0x00007fc8ac45443d c2iNCI: 0x00007fc8ac454498
- compiled entry 0x00007fc8ac45445e
- code size: 4
- code start: 0x00007fc87d0943c0
- code end (excl): 0x00007fc87d0943c4
- method data: 0x00007fc87d094578
- checked ex length: 0
- linenumber start: 0x00007fc87d0943c4
- localvar length: 0
------------------------ OptoAssembly for Compile_id = 85 -----------------------
#
# int ( int, int )
#
000 N1: # out( B1 ) <- in( B1 ) Freq: 1
000 B1: # out( N1 ) <- BLOCK HEAD IS JUNK Freq: 1
000 # stack bang (96 bytes)
pushq rbp # Save rbp
subq rsp, #16 # Create frame
01a leal RAX, [RSI + RDX]
01d addq rsp, 16 # Destroy frame
popq rbp
cmpq rsp, poll_offset[r15_thread]
ja #safepoint_stub # Safepoint: poll for GC
02c ret
--------------------------------------------------------------------------------
----------------------------------- Assembly -----------------------------------
Compiled method (c2) 8266 85 Test::test (4 bytes)
total in heap [0x00007fc8ac567888,0x00007fc8ac5679f8] = 368
relocation [0x00007fc8ac567970,0x00007fc8ac567980] = 16
main code [0x00007fc8ac567980,0x00007fc8ac5679d0] = 80
stub code [0x00007fc8ac5679d0,0x00007fc8ac5679e8] = 24
oops [0x00007fc8ac5679e8,0x00007fc8ac5679f0] = 8
metadata [0x00007fc8ac5679f0,0x00007fc8ac5679f8] = 8
immutable data [0x00007fc85c085a10,0x00007fc85c085a50] = 64
dependencies [0x00007fc85c085a10,0x00007fc85c085a18] = 8
scopes pcs [0x00007fc85c085a18,0x00007fc85c085a48] = 48
scopes data [0x00007fc85c085a48,0x00007fc85c085a50] = 8
[Disassembly]
--------------------------------------------------------------------------------
[Constant Pool (empty)]
--------------------------------------------------------------------------------
[Verified Entry Point]
# {method} {0x00007fc87d0943c8} 'test' '(II)I' in 'Test'
# parm0: rsi = int
# parm1: rdx = int
# [sp+0x20] (sp of caller)
;; N1: # out( B1 ) <- in( B1 ) Freq: 1
;; B1: # out( N1 ) <- BLOCK HEAD IS JUNK Freq: 1
0x00007fc8ac567980: mov %eax,-0x18000(%rsp)
0x00007fc8ac567987: push %rbp
0x00007fc8ac567988: sub $0x10,%rsp
0x00007fc8ac56798c: cmpl $0x0,0x20(%r15)
0x00007fc8ac567994: jne 0x00007fc8ac5679c3 ;*synchronization entry
; - Test::test@-1 (line 17)
0x00007fc8ac56799a: lea (%rsi,%rdx,1),%eax
0x00007fc8ac56799d: add $0x10,%rsp
0x00007fc8ac5679a1: pop %rbp
0x00007fc8ac5679a2: cmp 0x28(%r15),%rsp ; {poll_return}
0x00007fc8ac5679a6: ja 0x00007fc8ac5679ad
0x00007fc8ac5679ac: retq
0x00007fc8ac5679ad: movabs $0x7fc8ac5679a2,%r10 ; {internal_word}
0x00007fc8ac5679b7: mov %r10,0x498(%r15)
0x00007fc8ac5679be: jmpq 0x00007fc8ac500760 ; {runtime_call SafepointBlob}
0x00007fc8ac5679c3: callq Stub::nmethod_entry_barrier ; {runtime_call StubRoutines (final stubs)}
0x00007fc8ac5679c8: jmpq 0x00007fc8ac56799a
0x00007fc8ac5679cd: hlt
0x00007fc8ac5679ce: hlt
0x00007fc8ac5679cf: hlt
[Exception Handler]
0x00007fc8ac5679d0: jmpq 0x00007fc8ac500c60 ; {no_reloc}
[Deopt Handler Code]
0x00007fc8ac5679d5: callq 0x00007fc8ac5679da
0x00007fc8ac5679da: subq $0x5,(%rsp)
0x00007fc8ac5679df: jmpq 0x00007fc8ac501ba0 ; {runtime_call DeoptimizationBlob}
0x00007fc8ac5679e4: hlt
0x00007fc8ac5679e5: hlt
0x00007fc8ac5679e6: hlt
0x00007fc8ac5679e7: hlt
--------------------------------------------------------------------------------
[/Disassembly]
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Oops:
0x00007fc8ac5679e8: 0x00000006357f0c98 a 'com/sun/tools/javac/launcher/MemoryClassLoader'{0x00000006357f0c98}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Metadata:
0x00007fc8ac5679f0: 0x00007fc87d0943c8 {method} {0x00007fc87d0943c8} 'test' '(II)I' in 'Test'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
pc-bytecode offsets:
PcDesc(pc=0x00007fc8ac56797f offset=ffffffff bits=0):
PcDesc(pc=0x00007fc8ac56799a offset=1a bits=0):
Test::test@-1 (line 17)
PcDesc(pc=0x00007fc8ac5679e9 offset=69 bits=0):
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
oop maps:ImmutableOopMapSet contains 0 OopMaps
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
scopes:
ScopeDesc(pc=0x00007fc8ac56799a offset=1a):
Test::test@-1 (line 17)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
relocations:
@0x00007fc8ac567970: 5822
relocInfo@0x00007fc8ac567970 [type=11(poll_return) addr=0x00007fc8ac5679a2 offset=34]
@0x00007fc8ac567972: 780b400b
relocInfo@0x00007fc8ac567974 [type=8(internal_word) addr=0x00007fc8ac5679ad offset=11 data=11] | [target=0x00007fc8ac5679a2]
@0x00007fc8ac567976: 3111
relocInfo@0x00007fc8ac567976 [type=6(runtime_call) addr=0x00007fc8ac5679be offset=17 format=1] | [destination=0x00007fc8ac500760]
@0x00007fc8ac567978: 3105
relocInfo@0x00007fc8ac567978 [type=6(runtime_call) addr=0x00007fc8ac5679c3 offset=5 format=1] | [destination=0x00007fc8ac45ece0]
@0x00007fc8ac56797a: 000d
relocInfo@0x00007fc8ac56797a [type=0(none) addr=0x00007fc8ac5679d0 offset=13]
@0x00007fc8ac56797c: 3100
relocInfo@0x00007fc8ac56797c [type=6(runtime_call) addr=0x00007fc8ac5679d0 offset=0 format=1] | [destination=0x00007fc8ac500c60]
@0x00007fc8ac56797e: 310f
relocInfo@0x00007fc8ac56797e [type=6(runtime_call) addr=0x00007fc8ac5679df offset=15 format=1] | [destination=0x00007fc8ac501ba0]
@0x00007fc8ac567980:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Dependencies:
Dependency of type evol_method
method = *{method} {0x00007fc87d0943c8} 'test' '(II)I' in 'Test'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ExceptionHandlerTable (size = 0 bytes)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ImplicitExceptionTable is empty
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Recorded oops:
#0: 0x0000000000000000 nullptr-oop
#1: 0x00000006357f0c98 a 'com/sun/tools/javac/launcher/MemoryClassLoader'{0x00000006357f0c98}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Recorded metadata:
#0: 0x0000000000000000 nullptr-oop
#1: 0x00007fc87d0943c8 {method} {0x00007fc87d0943c8} 'test' '(II)I' in 'Test'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Done
------------------------------------------------------------------------
static Test::test(II)I
interpreter_invocation_count: 6784
invocation_counter: 6784
backedge_counter: 0
decompile_count: 0
mdo size: 384 bytes
0 iload_0
1 iload_1
2 iadd
3 ireturn
------------------------------------------------------------------------
Total MDO size: 384 bytes
Давайте посмотрим на некоторые детали.
#r018 rsi : parm 0: int
#r016 rdx : parm 1: int
Мы видим, что два аргумента типа int
компилируются таким образом, чтобы оказаться в CPU регистрах rsi
и rdx
.
Комментарий от редакции Spring АйО
rsi
, rdx
, а далее rax
— это все 64-битные регистры x86 CPU, которыми оперирует x86-ассемблер. При этом rax
часто используется в различных calling convention как регистр для возвращаемого значения функции.
То есть, на машине автора вызывающая функция main
будет ожидать возвращаемое значение функции test
в регистре rax
, что характерно для большинства машин и архитектур.
Регистры rsi
и rdx
, как правило (хотя и не всегда), являются регистрами общего назначения и хранят произвольную информацию.
Это будет интересно, когда мы будем смотреть на ассемблерный код. Давайте сначала посмотрим на OptoAssembly
, что представляет собой промежуточную форму ассемблерного типа, которая создается перед генерацией машинного кода:
------------------------ OptoAssembly for Compile_id = 85 -----------------------
#
# int ( int, int )
#
000 N1: # out( B1 ) <- in( B1 ) Freq: 1
000 B1: # out( N1 ) <- BLOCK HEAD IS JUNK Freq: 1
000 # stack bang (96 bytes)
pushq rbp # Save rbp
subq rsp, #16 # Create frame
01a leal RAX, [RSI + RDX]
01d addq rsp, 16 # Destroy frame
popq rbp
cmpq rsp, poll_offset[r15_thread]
ja #safepoint_stub # Safepoint: poll for GC
02c ret
Важные инструкции здесь следующие:
leal RAX, [RSI + RDX]
: по сути выполняетrax = rsi + rdx
, то есть складывает два аргумента метода.ret
возвращает значение вrax
.
Остальные инструкции относятся к поддержке стек-фреймов и выполнению safepoint-poll.
Отметим, что если вас интересует только ассемблерный код, вы можете напрямую использовать опцию -XX:CompileCommand=printassembly,Test::*
, которая опускает вывод OptoAssembly
.
Как таковой машинный код x64
находится в блоке, приведенном позднее:
[Verified Entry Point]
# {method} {0x00007fc87d0943c8} 'test' '(II)I' in 'Test'
# parm0: rsi = int
# parm1: rdx = int
# [sp+0x20] (sp of caller)
;; N1: # out( B1 ) <- in( B1 ) Freq: 1
;; B1: # out( N1 ) <- BLOCK HEAD IS JUNK Freq: 1
0x00007fc8ac567980: mov %eax,-0x18000(%rsp)
0x00007fc8ac567987: push %rbp
0x00007fc8ac567988: sub $0x10,%rsp
0x00007fc8ac56798c: cmpl $0x0,0x20(%r15)
0x00007fc8ac567994: jne 0x00007fc8ac5679c3 ;*synchronization entry
; - Test::test@-1 (line 17)
0x00007fc8ac56799a: lea (%rsi,%rdx,1),%eax
0x00007fc8ac56799d: add $0x10,%rsp
0x00007fc8ac5679a1: pop %rbp
0x00007fc8ac5679a2: cmp 0x28(%r15),%rsp ; {poll_return}
0x00007fc8ac5679a6: ja 0x00007fc8ac5679ad
0x00007fc8ac5679ac: retq
0x00007fc8ac5679ad: movabs $0x7fc8ac5679a2,%r10 ; {internal_word}
0x00007fc8ac5679b7: mov %r10,0x498(%r15)
0x00007fc8ac5679be: jmpq 0x00007fc8ac500760 ; {runtime_call SafepointBlob}
0x00007fc8ac5679c3: callq Stub::nmethod_entry_barrier ; {runtime_call StubRoutines (final stubs)}
0x00007fc8ac5679c8: jmpq 0x00007fc8ac56799a
0x00007fc8ac5679cd: hlt
0x00007fc8ac5679ce: hlt
0x00007fc8ac5679cf: hlt
[Exception Handler]
0x00007fc8ac5679d0: jmpq 0x00007fc8ac500c60 ; {no_reloc}
[Deopt Handler Code]
0x00007fc8ac5679d5: callq 0x00007fc8ac5679da
0x00007fc8ac5679da: subq $0x5,(%rsp)
0x00007fc8ac5679df: jmpq 0x00007fc8ac501ba0 ; {runtime_call DeoptimizationBlob}
Вам придется установить дизассемблер hsdis
, иначе вы будете видеть здесь только байты (см. этот блог-пост и эту вики).
Теперь этот код представлен в словесной форме, но он все еще напрямую описывает, что происходит в CPU. Опять же, наиболее важные для нас инструкции следующие:
lea (%rsi,%rdx,1),%eax
retq
Где-то ближе к концу мы опять находим bytecode:
0 iload_0
1 iload_1
2 iadd
3 ireturn
Я советую вам взять этот пример и немного с ним поиграться. Посмотрите, как изменения в методе Test::test
влияют на скомпилированный и на скомпилированный ассемблерный код.

Регистрируйтесь на главную конференцию про Spring на русском языке от сообщества Spring АйО! В мероприятии примут участие не только наши эксперты, но и приглашенные лидеры индустрии.