環顧四周,皆是對手!
- 雲時代的掉隊者,由於Java啟動的高延時、對資源的高佔用、導致在Serverless及FaaS架構下力不從心,在越來越流行的邊緣計算、IoT方向上也是難覓蹤影;
- Java語言在業務服務開發中孤獨求敗,但在系統級應用領域幾乎是C、C++、攪局者Go、黑天鵝Rust的天下;
- 移動應用、敏捷應用的追隨者,移動應用中Android逐步去Java,前端又是JS的世界,敏捷開發方面前有Ruby、Python後有NodeJS;
此時眾多的Javaer會不經意發問:學Java還有未來麼?
你可以嫌棄Java, 但是可以永遠相信JVM! 在雲原生如日中天、Serverless日漸成熟、新語言百花齊放的當下,跨語言、Native支援、高效能低資源佔用的技術必定是其璀璨的明珠,而GraalVM正是這樣一個承載了JVM未來,將Java帶入下一波技術的弄潮兒。
GraalVM - 雲原生時代的Java
“一次編寫,到處執行“是Java語言的特性,這一重要特性依靠的是JVM虛擬機器
在討論GraalVM之前,我們先聊下Java程式碼是怎麼執行的?
從硬體視角來看,Java位元組碼無法直接執行。因此,Java虛擬機器需要將位元組碼翻譯成機器碼。
從虛擬機器視角來看,執行Java程式碼首先需要將它編譯而成的 class 檔案載入到 Java 虛擬機器中。載入後的 Java 類會被存放於方法區(Method Area)中。實際執行時,虛擬機器會執行方法區內的程式碼。Java 虛擬機器同樣也在記憶體中劃分出堆和棧來儲存執行時資料。
不同的是,Java 虛擬機器會將棧細分為面向 Java 方法的 Java 方法棧,面向本地方法(用 C++ 寫的 native 方法)的本地方法棧,以及存放各個執行緒執行位置的 PC 暫存器。
在執行過程中,每當呼叫進入一個 Java 方法,Java 虛擬機器會在當前執行緒的 Java 方法棧中生成一個棧幀,用以存放區域性變數以及位元組碼的運算元。這個棧幀的大小是提前計算好的,而且 Java 虛擬機器不要求棧幀在記憶體空間裡連續分佈。
當退出當前執行的方法時,不管是正常返回還是異常返回,Java 虛擬機器均會彈出當前執行緒的當前棧幀,並將之捨棄。
GraalVM帶來哪些神奇的黑魔法?♂️
-
更快、更輕量化的應用
GraalVM的高效能JIT編譯器可以生成最佳化的本地機器程式碼,由於採用了先進的編譯器最佳化和積極複雜的內聯技術,執行速度更快,產生的垃圾更少,使用的CPU更少。最終的結果是應用程式執行速度更快,消耗的資源更少,從而降低了雲和基礎設施的成本。
-
Ahead-Of-Time(AOT)提前編譯技術
AOT 提前編譯,是相對於即時編譯而言的。AOT在執行過程中耗費 CPU 資源來進行即時編譯,而程式也能夠在啟動的瞬間就達到理想的效能。例如 C 和 C++語言採用的是AOT靜態編譯,直接將程式碼轉換成機器碼執行。而 Java 一直採用的是解釋 + 即時編譯技術。
GraalVM 的 AOT 編譯實際上是藉助了 SubstrateVM 編譯框架,可以將 SubstrateVM 理解為一個內嵌精簡版的 JVM,包含異常處理,同步,執行緒管理,記憶體管理(垃圾回收)和 JNI 等元件。
SubstrateVM 的啟動時間非常短,記憶體開銷非常少。用這種方式編譯出的 Java 程式的執行時間可與C語言持平。
-
語言互操作性
Graal VM 被官方稱為“Universal VM”和“Polyglot VM”,這是一個在 HotSpot 虛擬機器基礎上增強而成的跨語言全棧虛擬機器,可以作為“任何語言”的執行平臺使用,這裡“任何語言”包括了 Java、Scala、Groovy、Kotlin 等基於 Java 虛擬機器之上的語言,還包括了 C、C++、Rust 等基於 LLVM 的語言,同時支援其他像 JavaScript、Ruby、Python 和 R 語言等等。Graal VM 可以無額外開銷地混合使用這些程式語言,支援不同語言中混用對方的介面和物件,也能夠支援這些語言使用已經編寫好的本地庫檔案。
GraalVM在IoT邊緣側實現落地,效果顯著!
1、安裝無需JVM環境,30000行專案安裝僅需16M記憶體佔用,可在mac、linux-arm64、windows和樹莓派等硬體環境上執行;
2、專案啟動時間節省99%, 僅需10ms!享受極速的軟體體驗;
安裝
GraalVM在 SDKMAN上有多個版本,可透過sdk list java
進行檢視,我這裡使用的是22.2 社群版本
sdk install java 22.2.r11-grl
上述GraalVM安裝完成後,需要Native Image元件,使用下述命令列進行安裝:
Native Image安裝
gu install native-image
Maven專案設定
詳細graalvm-maven-plugin
使用簡介見:https://graalvm.github.io/native-build-tools/latest/maven-plugin-quickstart.html
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${graalvm-buildtools.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<skip>false</skip>
<imageName>${appName}</imageName>
<requiredVersion>${graalvm.version}</requiredVersion>
<mainClass>${mainClass}</mainClass>
<buildArgs>
<arg>--no-fallback</arg>
<arg>-Dfile.encoding=UTF-8</arg>
<arg>-H:-CheckToolchain</arg>
<arg>-H:+ReportExceptionStackTraces</arg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
當然GraalVM的 AOT提前編譯技術也不是完全沒有缺點,其對於多語言、反射和位元組碼動態載入技術需要相對繁瑣的配置。
在src/main/resources
資料夾下有
- native-image.properties //native image配置資訊檔案
- reflection-config.json //native 需要反射的類配置檔案
具體配置資訊詳情見: https://www.graalvm.org/22.0/reference-manual/native-image/Reflection/#manual-configuration
native-image.properties資訊如下:
Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json \
-H:IncludeLocales=zh,en,de,fr \
--initialize-at-run-time=io.netty.buffer.AbstractReferenceCountedByteBuf \
--initialize-at-run-time=io.netty.buffer.ByteBufAllocator \
--initialize-at-run-time=io.netty.buffer.ByteBufUtil \
--initialize-at-run-time=io.netty.buffer.ByteBufUtil$HexUtil \
--initialize-at-run-time=io.netty.buffer.PooledByteBufAllocator \
--initialize-at-run-time=io.netty.buffer.UnpooledHeapByteBuf \
--initialize-at-run-time=io.netty.buffer.UnreleasableByteBuf \
--initialize-at-run-time=io.netty.util.AbstractReferenceCounted \
--initialize-at-run-time=io.netty.util.internal.ThreadLocalRandom \
--initialize-at-run-time=io.netty.util.concurrent.GlobalEventExecutor \
--initialize-at-run-time=io.netty.util.concurrent.ImmediateEventExecutor \
--initialize-at-run-time=io.netty.util.concurrent.ScheduledFutureTask \
-Dio.netty.noUnsafe=true \
-Dio.netty.leakDetection.level=DISABLED
reflection-config.json資訊如下:
[
{
"name": "io.netty.channel.socket.nio.NioSocketChannel",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
]
},
......
{
"name" : "iot.technology.client.toolkit.nb.service.mobile.domain.action.data.MobCachedCommandResponse",
"allDeclaredMethods" : true,
"allPublicMethods" : true,
"allDeclaredClasses" : true,
"allPublicClasses" : true,
"allDeclaredConstructors" : true,
"allPublicConstructors" : true
}
]
上述資訊配置完成,我們在專案根目錄下執行以下命令:
mvn clean package -Pnative
會見到下面的日誌命令輸出:
[INFO] --- native-maven-plugin:0.9.17:compile-no-fork (build-native) @ toolkit-app ---
[INFO] Found GraalVM installation from JAVA_HOME variable.
......
========================================================================================================================
GraalVM Native Image: Generating 'toolkit' (executable)...
========================================================================================================================
[1/7] Initializing... (9.2s @ 0.20GB)
Version info: 'GraalVM 22.2.0 Java 17 CE'
Java version info: '17.0.4+8-jvmci-22.2-b06'
C compiler: cc (null, null, 0.0.0)
Garbage collector: Serial GC
1 user-specific feature(s)
- org.graalvm.home.HomeFinderFeature: Finds GraalVM paths and its version number
[2/7] Performing analysis... [*********] (45.3s @ 2.77GB)
11,123 (90.04%) of 12,354 classes reachable
18,642 (59.17%) of 31,506 fields reachable
57,906 (57.83%) of 100,137 methods reachable
524 classes, 194 fields, and 2,046 methods registered for reflection
79 classes, 196 fields, and 136 methods registered for JNI access
5 native libraries: -framework CoreServices, -framework Foundation, dl, pthread, z
[3/7] Building universe... (4.7s @ 1.45GB)
[4/7] Parsing methods... [**] (3.6s @ 1.88GB)
[5/7] Inlining methods... [***] (2.2s @ 3.94GB)
[6/7] Compiling methods... [*****] (31.6s @ 4.19GB)
[7/7] Creating image... (5.6s @ 2.77GB)
25.79MB (49.67%) for code area: 37,581 compilation units
25.84MB (49.78%) for image heap: 290,531 objects and 175 resources
292.13KB ( 0.55%) for other data
51.92MB in total
------------------------------------------------------------------------------------------------------------------------
Top 10 packages in code area: Top 10 object types in image heap:
1.65MB sun.security.ssl 5.53MB byte[] for code metadata
1021.89KB java.util 2.78MB java.lang.String
919.87KB picocli 2.73MB java.lang.Class
731.85KB com.sun.crypto.provider 2.62MB byte[] for general heap data
565.20KB java.lang.invoke 2.42MB byte[] for java.lang.String
518.23KB java.lang 1.46MB byte[] for embedded resources
504.26KB org.jline.reader.impl 955.88KB com.oracle.svm.core.hub.DynamicHubCompanion
476.05KB c.s.org.apache.xerces.internal.impl.xs.traversers 663.69KB byte[] for reflection metadata
458.58KB sun.security.x509 623.30KB java.util.HashMap$Node
438.98KB com.sun.org.apache.xerces.internal.impl 580.77KB java.lang.String[]
18.36MB for 389 more packages 4.51MB for 2478 more object types
------------------------------------------------------------------------------------------------------------------------
6.3s (5.7% of total time) in 32 GCs | Peak RSS: 5.93GB | CPU load: 5.50
------------------------------------------------------------------------------------------------------------------------
Produced artifacts:
......
GraalVM帶來的驚人效果
啟動時間
啟動時間程式碼新增:
public static void main(String[] args) {
long begin = System.currentTimeMillis();
......
try {
......
long end = System.currentTimeMillis();
long runTime = end - begin;
System.out.println("startUp cost runTime: " + runTime);
......
} finally {
......
}
}
使用IDEA啟動程式碼,啟動時間如下:
AOT提前編譯後native-image啟動時間:
➜ target git:(develop-v0.6.6) ✗ ./toolkit
......
startUp cost runTime: 11
佔用記憶體的效果
專案程式碼統計:
編譯後可在linux、樹莓派、mac系統和windows系統可執行的映象;
上述的專案配置和落地實踐專案見: https://github.com/IoT-Technology/IoT-Toolkit
結束語
GraalVM對於JVM的開發人員來說,無疑是個非常好的訊息。但是留給JVM的時間真的不多了。