
Ну, блин, короче
Знаете ли вы, куда уходит время и ресурсы при сборке проектов на Java? Сейчас покажем и расскажем, как сберечь время, нервы и кофе.
У сборочной системы Gradle есть интересный ключ —scan, позволяющий наглядно посмотреть на графике распределение ресурсов при сборке. После завершения процедуры протокол сборки загружается на сайт Gradle, и в красивом интерфейсе можно посмотреть на этапы. Там много другой информации, обязательно посмотрите, если ещё не видели.
Только имейте ввиду, что не всякий проект стоит так собирать – чувствительные данные, пароли и прочее отправлять в Интернет не надо.
Получаем что-то вроде такого:

В оригинале эта картинка «живая» и там можно многое узнать о сборке вашего проекта. Скриншот лишь показывает, что задач выполняется много. Давайте немного структурируем информацию. Я перерисовал этот же таймлайн и получилось уже более понятно. Заштрихованные части — задачи компиляции, незаштрихованные — остальные задачи (документация, подготовка jar и др.):

В разных проектах получается, что на компиляцию тратится где‑то 35–50% ресурсов.
Но мало того, ресурсы тратятся не только непосредственно на компиляцию, но ещё и на «разогрев» виртуальной машины.
Как вы знаете, javac
(компилятор языка Java) сам написан на Java (как говорят компиляторщики — раскручен, по‑английски bootstrapped). Поэтому и запускается он сравнительно небыстро.
Как запускается Java-приложение
Давайте вкратце вспомним, как происходит запуск Java-приложения. Это важно для понимания следующего шага.

-
Сначала запускается небольшой «пускач» (по-английски launcher). Это очень небольшое приложение (16 Кб), основная задача которого – загрузить и запустить библиотеку libJLI (Java launcher interface). Практически все утилиты, которые можно запустить из
$JDK/bin
– это копия launcher’а. Отличаются они, по сути, названием, и классом, который вызовется на одном из следующих шагов. -
Библиотека libJLI, в свою очередь, разбирает параметры командной строки, переменные окружения, определяет конфигурацию, в которой произошел запуск (JRE или JDK), собирает всю эту информацию воедино и, в зависимости от того, что за «пускач» был вызван, передает всю эту информацию в JVM.
И вот тут начинается магия.
-
JVM загружает класс и/или jar-файл (в последнем случае она, очевидно, разворачивает zip-архив в памяти), раскладывает класс(ы) в памяти, инициализирует их и запускает интерпретатор (“Исполнение” на картинке выше).
Процесс инициализации классов – вещь не бесплатная и занимает определенное время. Для ускорения этой задачи в JRE предусмотрены механизмы Class Data Sharing и Application Class Data Sharing, которые позволяют сохранить состояние классов и загрузить их в память, минуя этап инициализации классов.
Делается это парой ключей -XX:ArchiveClassesAtExit=<архив>
и -XX:SharedArchiveFile=<архив>
.
На первом шаге запускаем приложение с ключом -XX:ArchiveClassesAtExit=<архив>, даём ему настроиться (загрузить все классы), после чего останавливаем. Получится архив с настроенными классами. После этого следующие запуски производим с ключом -XX:SharedArchiveFile=<архив>, указав путь до прежде сохранённого архива. В зависимости от количества классов, это даёт возможность экономить время старта приложения.
-
В процессе исполнения кода методов в работу включаются два компилятора прямо внутри JVM – C1 и C2. Это части JIT (just-in-time) компилятора, преобразующие байткод в машинный код платформы, на которой запущена JVM. Отличаются они разным количеством оптимизаций и правилами, по которым производится включение их в работу. Описание работы C1/C2 сильно выходит за рамки настоящей статьи, но вы можете посмотреть то, что уже есть на Хабре.
На все перечисленные процедуры при старте Java-приложения нужно время и ресурсы, в том числе поэтому компиляция такая неспешная.
Ускоряем javac
В некоторых случаях старт Java-приложения можно ускорить, применив к нему AOT-компиляцию (ahead-of-time, компиляция перед исполнением) с помощью Axiom NIK Pro. Это компилятор GraalVM с патчами от нашей компании.
После выполнения AOT-компиляции Java-приложение представляет собой машинный код конкретной платформы, поэтому при запуске преобразованного приложения не тратится время на разогрев.
Как следствие, приложение стартует, да и работает заметно быстрее исходного, нескомпилированного приложения.
Мы скомпилировали компилятор Javac по такой схеме, и он стал работать быстрее практически вдвое. Суммарное ускорение при сборке проектов составляет 10-20%, поскольку сборка проектов состоит не только из компиляции + тесты, если они есть, могут работать часами.
Причину ускорения наглядно можно увидеть на графиках, полученных из снятых профилей производительности при компиляции модуля jdk.compiler
(это классическая задача “раскрутки” компилятора, когда разрабатываемый компилятор собирает свои же собственные исходные тексты).
Красным цветом показаны расход памяти и процессора оригинальным компилятором Javac при компиляции собственных исходников, зелёным – этот же компилятор, но уже после AOT. Разница весьма заметная.
Цифры для графиков получены с помощью psrecord:
psrecord --log-format=csv --log=compile.csv --include-children "mvn compile"


Такое ускорение получается за счёт преобразования байткода в машинный код и отсутствия необходимости в JIT-компиляции, на которую тратятся значительные ресурсы.
Ускоренная версия javac включена в состав продукта Axiom JDK Express, который доступен в личном кабинете в статусе раннего доступа, можете скачивать и использовать.
Для использования напрямую (при вызове javac
из командной строки) не требуется дополнительной настройки. Для использования в проектах Maven и Gradle необходимо включить режим fork
в конфигурации.
Maven
В pom.xml нужно добавить профиль
<profile> <id>native_build</id> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.10.1</version> <configuration> <fork>true</fork> <executable>/путь/до/jdk/bin/javac</executable> </configuration> </plugin> </plugins> </build> </profile>
Запуск сборки выполняется как mvn <target> -Pnative_build
.
Gradle
Создаете в проекте файл customjavac.gradle.kts с содержимым
allprojects { plugins.withId("java-base") { tasks.withType<JavaCompile>().configureEach { options.forkOptions.executable = "/путь/до/jdk/bin/javac" options.isFork = true } } }
И далее запускаете сборку как ./gradlew --init-script customjavac.gradle.kts <target>
.
P.S. Особенная благодарность Владимиру Ситникову @vladimirsitnikov за подсказку в настройке Gradle.
ссылка на оригинал статьи https://habr.com/ru/articles/898460/
Добавить комментарий