- PVSM.RU - https://www.pvsm.ru -
Привет! В свете не самых давних новостей про политику Oracle относительно лицензирования джавы всё острее встаёт вопрос ухода от оракловых версий в сторону OpenJDK. Оданко в OracleLabs уже давно делают весьма крутую штуку под названием GraalVM [1], который представляет из себя крутой JIT-компилятор, написанный на джаве, а также рантайм для запуска кода на таких языках как JavaScript, Ruby, Python, C, C++, Scala, Kotlin, R, Clojure. Впечатляет, правда? Но не о крутоте полиглот-среды я хочу вам рассказать. Речь пойдёт про сложности вкорячивания самой свежей сборки грааля в экосиситему OpenJDK 11 и чуток про производительность, совсем чуток…
История моего знакомства с graalvm началась на джокере [2] в 2017 году. Там Chris Seaton [3] очень подробно рассказал про внутренности компилятора и показал магию AOT компиляции на примере использования native-image из поставки грааля (это такая шутка, которая компилит твой джава код в нативный бинарник).
После того доклада я очень долго тренировался на компиляции нативного бинарника своего пет-проджекта, ставил костыли и грабли для того, чтобы заработал reflection во всех местах (будь он не ладен!) и, наконец, наткнулся на нерешенные проблемы с IO (что-то там не взлетело с zookeeper'ом, сейчас уже и не вспомню что). Плюнул пока на native-image :-(
Год 2018, всё тот же джокер и тот же graalvm в суперподробном докладе от Олега Шелаева про AOT.
В докладах и презентациях всё так здорово смотрится, а ещё на диске валяется пет-проджект… пора расчехлять терминал, качнуть свеженький релиз-кандидат грааля и в бой! Будем JIT трогать.
Трогать и пинать свеженький JIT (на момент написания статьи — это версия ce-1.0.0-rc14) будем на примере куска кода для тестирования производительности с сайта https://graalvm.org [4] — первый пример наш.
Какой же нынче джава-проект (даже Hello World) обходится без какой либо системы сборки? Правильно, только тот, на котором учатся готовить javac. Джавак готовить мы учиться не будем, пусть джаваком рулит maven.
Итак, встречайте, pom.xml:
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<!--<packaging>jar</packaging>-->
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>my-app</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>11</java.version>
<graalvm.version>1.0.0-rc14</graalvm.version>
</properties>
<profiles>
<profile>
<id>jdk11</id>
<activation>
<jdk>11</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.10</version>
<executions>
<execution>
<id>copy</id>
<phase>process-test-classes</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>com.mycompany.app.App</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<release>${java.version}</release>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.graalvm.compiler</groupId>
<artifactId>compiler</artifactId>
<version>${graalvm.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.truffle</groupId>
<artifactId>truffle-api</artifactId>
<version>${graalvm.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>${graalvm.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${graalvm.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>${graalvm.version}</version>
</dependency>
</dependencies>
</project>
Структура файлов проекта выглядит так:
Код класса com.mycompany.app.App (копипаст примера с graalvm.org):
package com.mycompany.app;
public class App {
static final int ITERATIONS = Math.max(Integer.getInteger("iterations", 1), 1);
public static void main(String[] args) {
String sentence = String.join(" ", args);
for (int iter = 0; iter < ITERATIONS; iter++) {
if (ITERATIONS != 1) System.out.println("-- iteration " + (iter + 1) + " --");
long total = 0, start = System.currentTimeMillis(), last = start;
for (int i = 1; i < 10_000_000; i++) {
total += sentence.chars().filter(Character::isUpperCase).count();
if (i % 1_000_000 == 0) {
long now = System.currentTimeMillis();
System.out.printf("%d (%d ms)%n", i / 1_000_000, now - last);
last = now;
}
}
System.out.printf("total: %d (%d ms)%n", total, System.currentTimeMillis() - start);
}
}
}
Код module-info.java:
module com.mycompany.app {}
Хм, пустой… А пустой он по той причине, что я хочу показать вам, как кастомная java (про кастомную джаву чуть позже) будет ругаться при необъявленных модулях, которые нужны нашему приложению.
не комом. Давайте соберём наш проект и запустим его. Собираем так:
mvn clean package
Запускаем:
$JAVA_HOME/bin/java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Diterations=10 -jar target/my-app-1.0-SNAPSHOT.jar In 2017 I would like to run ALL languages in one VM.
Тут есть два важных момента: JVMCI — экспериментальная штука, появившаяся в джаве с 9-ой версии, поэтому нам нужно:
-XX:+UnlockEperimentalVMOptions
Что мы тут видим? А видим то, что первая итерация самая долгая (3,5 секунды), это JIT разогревается. А дальше всё более или менее ровно (в пределах одной секунды).
А что если дадим джаве свежую версию грааля? Сказано — сделано:
java -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Diterations=10 --module-path=target/lib --upgrade-module-path=target/lib/compiler-1.0.0-rc14.jar -jar target/my-app-1.0-SNAPSHOT.jar In 2017 I would like to run ALL languages in one VM.
Результат, как мы видим, несильно отличается.
Забыл. Мы ж не попробовали запустить то же самое без новмодного JIT-компилятора. Сделаем:
java -Diterations=10 -jar target/my-app-1.0-SNAPSHOT.jar In 2017 I would like to run ALL languages in one VM.
Результат отличается, и прилично.
С2 не даёт никаких оптимизаций на горячих кусках кода — каждая итерация с одним и тем же временем.
Graal же умеет оптимизировать горячие куски кода и в перспективе даёт хороший прирост производительности.
Это, пожалуй, главный вопрос, который необходимо задавать себе и другим (участникам команды), когда в проект хотим добавить новую фичу, новый тул, новую вирутальную машину, новый JIT…
Моя история, как писалось выше, началась с Joker 2017, потом были долгие попытки осилить AOT, а теперь вот вкушаю прелести JIT'а для джавы на джаве.
Пет-проджект на диске представляет из себя некий движок бизнес-процессов, где процессы рисуются прикладными разработчиками в UI в браузере, и у них есть возможность писать скрипты на JS, которые будут бежать на JVM.
В будущих версиях джавы nashorn обещают убрать, GraalVM постепенно близится к релизу…
Что ж, ответ на вопрос такой:
--module-path
и
--upgrade-module-path
, но со свежей сборкой грааля)
Первые два пункта в списке ответов на поставленный выше вопрос достаточно понятны, давайте разбираться с последним.
Дело в том, что разработчики-админы-девопсы люди ленивые и делать лишнюю работу не любят (я тоже такой), пытаются всё автоматизировать и запаковать в готовый бандл, который можно запустить как простой бинарь. Что ж, задача есть, давайте её решать.
На помощь к нам приходит относительно новый тул из мира Java 9+, и имя ему jlink [5]. Пробуем запаковать наше приложение со всеми необходимыми либами в бандл:
jlink --module-path target/classes:target/lib:$JAVA_HOME/jmods --add-modules com.mycompany.app --launcher app=com.mycompany.app/com.mycompany.app.App --compress 2 --no-header-files --no-man-pages --strip-debug --output test
Как много параметров всяких, опишем основные:
--module-path target/classes:target/lib:$JAVA_HOME/jmods
--add-modules com.mycompany.app
--launcher app=com.mycompany.app/com.mycompany.app.App
--output test
Про остальные параметры можно спросить у дядюшки гугла, все они направлены на то, чтобы уменьшить итоговый размер бандла.
Посмотрим на результат:
Внутри test/bin/app лежит простой sh-скрипт, который запускает наше приложение на той джаве, что лежит рядом с app:
#!/bin/sh
JLINK_VM_OPTIONS="-Diterations=10" #системный параметр мы уже ручками задефайнили, изначально переменная тут с пустым значением
DIR=`dirname $0`
$DIR/java $JLINK_VM_OPTIONS -m com.mycompany.app/com.mycompany.app.App $@
Запустим test/bin/app на C2:
./test/bin/app In 2017 I would like to run ALL languages in one VM.
Теперь на graalvm (определив необходимые для запуска флаги в переменной JLINK_VM_OPTIONS):
#!/bin/sh
JLINK_VM_OPTIONS="-XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler -Diterations=10"
DIR=`dirname $0`
$DIR/java $JLINK_VM_OPTIONS -m com.mycompany.app/com.mycompany.app.App $@
Результат:
Error occurred during initialization of boot layer
java.lang.module.FindException: Module jdk.internal.vm.ci not found
Ну вот, приплыли… А теперь вспомним, что мы работаем с java 11 в модульном окружении, собираем приложение как модуль, а про используемые модули ничего никому не сказали. Пора исправляться.
Новая версия module-info.java:
module com.mycompany.app {
requires jdk.internal.vm.compiler;
requires org.graalvm.sdk;
requires org.graalvm.truffle;
requires transitive org.graalvm.js;
requires transitive org.graalvm.js.scriptengine;
}
Собираем [6], удаляем директорию test, линкуем [7].
Результат:
Error: automatic module cannot be used with jlink: icu4j from file:///home/slava/JavaProjects/graal-js-jdk11-maven-demo/target/lib/icu4j-62.1.jar
Что за «автоматик модуль кеннот би юзд»? А это jlink нам говорит, что либа icu4j не cодержит в себе module-info.class. Что нужно, чтобы такой класс появился внутри указанной либы:
Поехали!
Файл module-info.java со всем содержимым сгенерирует для наc утилита jdeps из состава openjdk-11:
Компилируем module-info.java для либы icu4j:
Обновляем джарник, заталкивая в него module-info.class:
$JAVA_HOME/bin/jar uf target/lib/icu4j-62.1.jar -C target/modules module-info.class
УРА! У нас получилось! Теперь мы имеем забандленное приложение в виде запускаемого sh-скрипта со своей джавой, со всеми необходимыми модулями (включая свежий graalvm), с преферансом и барышнями.
Java не даёт скучать и даёт новую пищу для ума с каждым релизом. Пробуйте новые фичи, экспериментируйте, делитесь опытом. Надеюсь, скоро напишу статью про то как забандлил часть пет-проджекта с граалем (там vert.x, асинхронщина и js-срипты — будет интересно).
И ещё… это моя первая статья на Хабре, — прошу, не сильно бейте.
Автор: zhulikovatyi
Источник [9]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/maven/313064
Ссылки в тексте:
[1] GraalVM: https://graalvm.org
[2] джокере: https://jokerconf.com/
[3] Chris Seaton: https://2017.jokerconf.com/en/2017/talks/ghdvtsu3y60qai68waayi/
[4] https://graalvm.org: https://www.graalvm.org/docs/examples/java-performance-examples/
[5] jlink: https://docs.oracle.com/en/java/javase/11/tools/jlink.html
[6] Собираем: #build
[7] линкуем: #link
[8] запускаем: #run
[9] Источник: https://habr.com/ru/post/445978/?utm_campaign=445978
Нажмите здесь для печати.