Перейти к основному содержимому

Как разогреть JVM

· 6 мин. чтения

1. Обзор

JVM — одна из старейших, но мощных виртуальных машин, когда-либо созданных.

В этой статье мы кратко рассмотрим, что значит разогреть JVM и как это сделать.

2. Основы архитектуры JVM

Всякий раз, когда запускается новый процесс JVM, все необходимые классы загружаются в память экземпляром ClassLoader . Этот процесс происходит в три этапа:

  1. Загрузка классов Bootstrap: « Загрузчик классов Bootstrap » загружает код Java и основные классы Java, такие как java.lang.Object , в память. Эти загруженные классы находятся в JRE\lib\rt.jar .
  2. Загрузка класса расширения : ExtClassLoader отвечает за загрузку всех файлов JAR, расположенных по пути java.ext.dirs . В приложениях, отличных от Maven или Gradle, где разработчик добавляет файлы JAR вручную, все эти классы загружаются на этом этапе.
  3. Загрузка класса приложения : AppClassLoader загружает все классы, расположенные в пути к классу приложения.

Этот процесс инициализации основан на схеме ленивой загрузки.

3. Что разогревает JVM

После завершения загрузки классов все важные классы (используемые во время запуска процесса) помещаются в кеш JVM (собственный код) , что ускоряет доступ к ним во время выполнения. Другие классы загружаются по запросу.

Первый запрос к веб-приложению Java часто значительно медленнее, чем среднее время отклика за время существования процесса. Этот период разогрева обычно можно объяснить ленивой загрузкой классов и своевременной компиляцией.

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

Этот процесс настройки JVM известен как прогрев.

4. Многоуровневая компиляция

Благодаря надежной архитектуре JVM часто используемые методы загружаются в собственный кэш в течение жизненного цикла приложения.

Мы можем использовать это свойство для принудительной загрузки критических методов в кеш при запуске приложения. Для этого нам нужно установить аргумент VM с именем Tiered Compilation :

-XX:CompileThreshold -XX:TieredCompilation

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

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

Приложения, работающие на JBoss и JDK версии 7 с включенным аргументом VM, имеют тенденцию к сбою через некоторое время из-за задокументированной ошибки . Проблема была исправлена в JDK версии 8.

Еще один момент, на который следует обратить внимание, заключается в том, что для принудительной загрузки мы должны убедиться, что все (или большинство) классов, которые должны быть выполнены, должны быть доступны. Это похоже на определение покрытия кода во время модульного тестирования. Чем больше кода покрыто, тем лучше будет производительность.

В следующем разделе показано, как это можно реализовать.

5. Ручная реализация

Мы можем реализовать альтернативный метод для разогрева JVM. В этом случае простая ручная разминка может включать в себя повторение создания разных классов тысячи раз сразу после запуска приложения.

Во-первых, нам нужно создать фиктивный класс с обычным методом:

public class Dummy {
public void m() {
}
}

Затем нам нужно создать класс со статическим методом, который будет выполняться не менее 100 000 раз при запуске приложения, и при каждом выполнении он создает новый экземпляр вышеупомянутого фиктивного класса, который мы создали ранее:

public class ManualClassLoader {
protected static void load() {
for (int i = 0; i < 100000; i++) {
Dummy dummy = new Dummy();
dummy.m();
}
}
}

Теперь, чтобы измерить прирост производительности , нам нужно создать основной класс. Этот класс содержит один статический блок, содержащий прямой вызов метода load() класса ManualClassLoader .

Внутри основной функции мы еще раз вызываем метод load() ManualClassLoader и фиксируем системное время в наносекундах непосредственно перед и после вызова нашей функции. Наконец, мы вычитаем это время, чтобы получить фактическое время выполнения.

Мы должны запустить приложение дважды; один раз с вызовом метода load() внутри статического блока и один раз без вызова этого метода:

public class MainApplication {
static {
long start = System.nanoTime();
ManualClassLoader.load();
long end = System.nanoTime();
System.out.println("Warm Up time : " + (end - start));
}
public static void main(String[] args) {
long start = System.nanoTime();
ManualClassLoader.load();
long end = System.nanoTime();
System.out.println("Total time taken : " + (end - start));
}
}

Ниже результаты воспроизведены в наносекундах:

            | **С разминкой**    | **Без разогрева**    | **Разница(%)**   | 
| 1220056 | 8903640 | 730 |
| 1083797 | 13609530 | 1256 |
| 1026025 | 9283837 | 905 |
| 1024047 | 7234871 | 706 |
| 868782 | 9146180 | 1053 |

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

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

6. Инструменты

Мы также можем использовать несколько инструментов для разогрева JVM. Одним из самых известных инструментов является Java Microbenchmark Harness, JMH . Он обычно используется для микро-бенчмаркинга. После загрузки он неоднократно обращается к фрагменту кода и отслеживает цикл итераций прогрева.

Чтобы использовать его, нам нужно добавить еще одну зависимость в pom.xml :

<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.33</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.33</version>
</dependency>

Мы можем проверить последнюю версию JMH в Central Maven Repository .

В качестве альтернативы мы можем использовать плагин JMH maven для создания примера проекта:

mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=com.foreach \
-DartifactId=test \
-Dversion=1.0

Далее создадим основной метод:

public static void main(String[] args) 
throws RunnerException, IOException {
Main.main(args);
}

Теперь нам нужно создать метод и аннотировать его аннотацией JMH @Benchmark :

@Benchmark
public void init() {
//code snippet
}

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

7. Тест производительности

За последние 20 лет большая часть вклада в Java была связана с GC (сборщиком мусора) и JIT (Just In Time Compiler). Почти все тесты производительности, найденные в Интернете, выполняются на JVM, уже запущенном в течение некоторого времени. Однако,

Тем не менее, Beihang University опубликовал сравнительный отчет с учетом времени прогрева JVM. Они использовали системы на базе Hadoop и Spark для обработки массивных данных:

./f6faee0be344b53eb1e9c77ea6daa445.png

Здесь HotTub обозначает среду, в которой была прогрета JVM.

Как видите, ускорение может быть значительным, особенно для относительно небольших операций чтения — вот почему эти данные интересно рассмотреть.

8. Заключение

В этой быстрой статье мы показали, как JVM загружает классы при запуске приложения и как мы можем разогреть JVM, чтобы повысить производительность.

В этой книге содержится дополнительная информация и рекомендации по теме, если вы хотите продолжить.

И, как всегда, полный исходный код доступен на GitHub .