В этой статье мы сначала представим процесс выполнения Java, а затем перейдем к обсуждению компилятора в реальном времени.В следующей части будет представлен механизм многоуровневой компиляции и, наконец, представлено влияние компилятора в реальном времени на запуск приложения. представление.
Содержание этой статьи основано на виртуальной машине HotSpot, и место, где разработана версия Java, будет объяснено в тексте.
0 Процесс выполнения Java-программы
На собеседовании по Java один из вопросов задавался так: Java-программа интерпретируется и выполняется или компилируется и выполняется?
Когда мы впервые изучали Java, мы, вероятно, думали, что Java компилируется и выполняется. На самом деле в Java есть как интерпретируемое, так и компилируемое выполнение.
Обычный процесс выполнения Java-программы выглядит следующим образом:
Файл исходного кода .java компилируется в байт-код .class с помощью команды javac, а затем выполняется с помощью команды java.
Следует отметить, что по принципу компиляции компиляцию принято делить на front-end и back-end. Внешний интерфейс выполнит лексический анализ, синтаксический анализ и семантический анализ программы, а затем сгенерирует промежуточную форму выражения (называемую IR: Intermediate Representation). Серверная часть расскажет об этом промежуточном выражении для оптимизации и, наконец, сгенерирует целевой машинный код.
В Java то, что генерируется после javac, является промежуточным выражением (.class), например
public class JITDemo2 {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
Приведенный выше код декомпилируется javap следующим образом:
// javap -c JITDemo2.class
Compiled from "JITDemo2.java"
public class com.example.demo.jitdemo.JITDemo2 {
public com.example.demo.jitdemo.JITDemo2();
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 #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
Когда JVM выполняется, она сначала считывает IR-инструкции одну за другой для выполнения, и этот процесс представляет собой процесс интерпретации и выполнения. Когда количество вызовов метода достигает порога, определяемого JIT-компиляцией, он инициирует JIT-компиляцию, в это время JIT-компилятор оптимизирует IR и генерирует машинный код. этого метода. Если этот метод вызывается позже, машина будет вызвана напрямую. Выполнение кода, это процесс компиляции и выполнения.
Итак, от файла .java до финального исполнения процесс примерно такой:
(CodeCache будет представлен ниже)
Итак, когда начинать своевременную компиляцию? Что представляет собой процесс своевременной компиляции? Продолжаем изучение ниже.
1 Предварительное изучение JIT-компилятора Java
Виртуальная машина HotSpot имеет два компилятора, называемые компиляторами C1 и C2 (после Java 10 был добавлен новый компилятор Graal).
Компилятору С1 соответствует параметр -client.Для программ с малым временем выполнения и требованиями к производительности запуска можно выбрать С1.
Компилятору C2 соответствует параметр -server.Для программ, требующих максимальной производительности, можно выбрать C2.
Но будь то -клиент или -сервер, C1 и C2 участвуют в работе по компиляции. Этот метод становится смешанным режимом (mixed), который также является методом по умолчанию, что видно по java -версии:
C:\Users\Lord_X_>java -version
java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)
Смешанный режим в последней строке иллюстрирует это.
Также мы можем принудительно использовать интерпретируемый режим через параметр -Xint, в это время компилятор реального времени вообще не участвует в работе, а последняя строка java -version будет отображать интерпретируемый режим.
Можно с помощью параметра -Xcomp принудительно указать, что используется только режим компиляции.В это время все коды будут скомпилированы сразу после старта программы.Этот метод замедлит время запуска, но после запуска выполнение интерпретации и компиляция C1 и C2 опущены.Время, эффективность выполнения кода будет значительно улучшена. На этом этапе последняя строка java -version будет отображать скомпилированный режим.
Давайте сравним эффективность выполнения трех режимов с помощью фрагмента кода (простая производительность):
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 99999999){
count += plus();
}
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
private static int plus() {
return random.nextInt(10);
}
}
- Первый - это режим выполнения чистой интерпретации.
Добавьте параметры виртуальной машины: -Xint -XX:+PrintCompilation (распечатать информацию о компиляции)
Результаты:
Информация о компиляции не выводится, что свидетельствует о том, что JIT-компилятор в работе не участвует.
- Тогда это чистый режим выполнения компиляции
Добавьте параметры виртуальной машины: -Xcomp -XX:+PrintCompilation
Результаты:
будет генерировать много информации о компиляции
- И, наконец, режим наложения
Добавьте параметры виртуальной машины: -XX:+PrintCompilation
Результаты:
Вывод: порядок, отнимающий много времени, таков: чистый режим интерпретации > чистый режим компиляции > смешанный режим.
Но вот только очень короткая программа.Если это долгоиграющая программа, я не знаю, будет ли эффективность выполнения чистого режима компиляции выше, чем у смешанного режима, и этот метод тестирования не является строгим, лучшим метод должен быть строгим эталонным тестом.следующий тест.
2 Когда запускать своевременную компиляцию
Триггеры компилятора «точно в срок» основаны на двух аспектах:
- вызовы методов
- Количество выполнений петли изнаночной кромки
Когда JVM вызывает метод, он добавит к счетчику 1. Если метод имеет тело цикла, счетчик также будет увеличиваться на 1 при каждом цикле.
Когда многоуровневая компиляция не включена (описано в следующей статье), когда счетчик метода достигает значения, заданного параметром -XX:CompileThreshold (C1 — 1500, C2 — 10000), будет запущена JIT-компиляция .
Давайте проведем эксперимент, запускаемый JIT-компиляцией, когда многоуровневая компиляция отключена:
- Первый запускается на основе вызовов методов (без циклов)
// 参数:-XX:+PrintCompilation -XX:-TieredCompilation(关闭分层编译)
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 15000){
System.out.println(i);
count += plus();
}
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
// 调用时,编译器计数器+1
private static int plus() {
return random.nextInt(10);
}
}
Результат выполнения следующий:
Так как работа подсчета при интерпретации и выполнении строго не синхронизирована с компилятором, она не будет строго 10000. Фактически, пока количество вызовов достаточно велико, это можно рассматривать как горячий код, и нет нужно строго синхронизировать.
- По заднему краю петли
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
plus();
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
// 调用时,编译器计数器+1
private static int plus() {
int count = 0;
// 每次循环,编译器计数器+1
for (int i = 0; i < 15000; i++) {
System.out.println(i);
count += random.nextInt(10);
}
return random.nextInt(10);
}
}
Результаты:
- На основе вызовов методов и обратных краев цикла
PS: в каждом вызове метода есть 10 циклов, поэтому каждый счетчик вызова метода должен +11, поэтому он должен запускать компиляцию точно в срок, когда количество вызовов превышает 10000/11=909.
public class JITDemo2 {
private static Random random = new Random();
public static void main(String[] args) {
long start = System.currentTimeMillis();
int count = 0;
int i = 0;
while (i++ < 15000) {
System.out.println(i);
count += plus();
}
System.out.println("time cost : " + (System.currentTimeMillis() - start));
}
// 调用时,编译器计数器+1
private static int plus() {
int count = 0;
// 每次循环,编译器计数器+1
for (int i = 0; i < 10; i++) {
count += random.nextInt(10);
}
return random.nextInt(10);
}
}
Результаты:
3 CodeCache
CodeCache — это временное хранилище для горячего кода, и код, скомпилированный компилятором реального времени, будет размещен здесь, и он существует в памяти вне кучи.
Параметры -XX:InitialCodeCacheSize и -XX:ReservedCodeCacheSize указывают размер памяти CodeCache.
- -XX:InitialCodeCacheSize: исходный размер памяти CodeCache, по умолчанию 2496 КБ.
- -XX:ReservedCodeCacheSize: размер зарезервированной памяти CodeCache, по умолчанию 48M
PS: Вы можете распечатать значения всех параметров по умолчанию через -XX:+PrintFlagsFinal.
3.1 Мониторинг CodeCache через jconsole
Вы можете увидеть расположение CodeCache в памяти с помощью инструмента jconsole, который поставляется с JDK, например
Из графика на рисунке видно, что CodeCache использовал более 4M.
3.2 Что происходит, когда CodeCache заполнен
Обычно, когда мы выделяем память под приложение, CodeCache часто игнорируется, хотя CodeCache занимает не так много места в памяти, и у него тоже есть GC, но он часто не заполняется. Но как только CodeCache заполнен, это может иметь катастрофические последствия для приложения с высоким QPS и высокими требованиями к производительности.
Из вышеприведенного введения мы знаем, что JVM сначала попытается интерпретировать и выполнить байт-код Java.Когда вызов метода или повторный цикл достигает определенного количества раз, он запускает компиляцию точно в срок для компиляции байт-кода Java. в локальный машинный код для улучшения выполнения. Скомпилированный собственный машинный код кэшируется в CodeCache.Если есть большой объем кода, который вызывает компиляцию точно в срок, и нет своевременной сборки мусора, CodeCache заполнится.
После заполнения CodeCache скомпилированный код по-прежнему будет выполняться как собственный код, но код, который не будет скомпилирован позже, может выполняться только как интерпретируемое выполнение.
Из сравнения в разделе 2 можно ясно увидеть разницу в производительности между интерпретируемым и скомпилированным выполнением. Так что для большинства приложений возникновение этой ситуации является катастрофическим.
Когда CodeCache заполняется, JVM печатает журнал:
JVM предоставляет метод GC для CodeCache: -XX:+UseCodeCacheFlushing. Этот параметр включен по умолчанию после JDK1.7.0_4 и будет пытаться перезапуститься, когда CodeCache вот-вот заполнится. JDK7 много перерабатывает в этой области, доход от сборщика мусора низкий, и он был значительно улучшен в JDK8, поэтому вы можете напрямую улучшить производительность в этой области, обновившись до JDK8.
3.3 Переработка CodeCache
Итак, когда можно переработать скомпилированный код в CodeCache?
Он начинается с того, как компилирует компилятор. Например, следующий код:
public int method(boolean flag) {
if (flag) {
return 1;
} else {
return 0;
}
}
С точки зрения интерпретации-исполнения процесс его исполнения выглядит следующим образом:
Однако код, скомпилированный компилятором реального времени, не обязательно такой. Компилятор реального времени соберет много информации о выполнении перед компиляцией. Например, если все значения флагов, введенные перед этим кодом, верны, компилятор реального времени может видоизмениться в следующее:
public int method(boolean flag) {
return 1;
}
То есть, как показано на рисунке ниже
Но это не всегда может быть flag=true позже. После того, как флаг передан false, это неправильно. В это время компилятор «деоптимизирует» его и превратит в метод выполнения компиляции. Производительность в журнале сделано не вступившим:
В это время метод больше не может быть введен. Когда JVM обнаружит, что все потоки вышли из скомпилированного, сделанного не входным, он пометит метод как: сделанный зомби, В это время память, занимаемая этим кодом, может быть переработана. Это видно из журнала компиляции:
3.4 Настройка CodeCache
Параметр запуска JVM предоставляется в Java8: -XX:+PrintCodeCache, который может печатать использование CodeCache, когда JVM останавливается.Вы можете наблюдать это значение каждый раз, когда останавливаете приложение, и медленно подгонять его до наиболее подходящего размера.
Давайте проиллюстрируем демонстрацию SpringBoot:
// 启动参数:-XX:ReservedCodeCacheSize=256M -XX:+PrintCodeCache
@RestController
@SpringBootApplication
public class DemoApplication {
// ... other code ...
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
System.out.println("start....");
System.exit(1);
}
}
Здесь я определяю CodeCache как 256M и распечатываю использование CodeCache при выходе из JVM, журнал выглядит следующим образом:
Используется максимум 6721K (max_used), что тратит много памяти, в это время можно попробовать уменьшить -XX:ReservedCodeCacheSize=256M и выделить лишнюю память в другие места.
4 Справочные документы
[1] https://blog.csdn.net/yandaonan/article/details/50844806
[2] Глубокое понимание виртуальной машины Java Чжоу Чжимин Глава 11
[3] Geek Time «Глубокий разбор виртуальной машины Java» Чжэн Юди
Добро пожаловать в мой публичный аккаунт WeChat