GraalVM(雲原生時代的Java)和IoT在邊緣側落地與實踐

三升水發表於2023-04-25

環顧四周,皆是對手!

  • 雲時代的掉隊者,由於Java啟動的高延時、對資源的高佔用、導致在Serverless及FaaS架構下力不從心,在越來越流行的邊緣計算、IoT方向上也是難覓蹤影;
  • Java語言在業務服務開發中孤獨求敗,但在系統級應用領域幾乎是C、C++、攪局者Go、黑天鵝Rust的天下;
  • 移動應用、敏捷應用的追隨者,移動應用中Android逐步去Java,前端又是JS的世界,敏捷開發方面前有Ruby、Python後有NodeJS;

多語言比拼.png

此時眾多的Javaer會不經意發問:學Java還有未來麼?

你可以嫌棄Java, 但是可以永遠相信JVM! 在雲原生如日中天、Serverless日漸成熟、新語言百花齊放的當下,跨語言、Native支援、高效能低資源佔用的技術必定是其璀璨的明珠,而GraalVM正是這樣一個承載了JVM未來,將Java帶入下一波技術的弄潮兒。

GraalVM - 雲原生時代的Java

“一次編寫,到處執行“是Java語言的特性,這一重要特性依靠的是JVM虛擬機器

在討論GraalVM之前,我們先聊下Java程式碼是怎麼執行的?

從硬體視角來看,Java位元組碼無法直接執行。因此,Java虛擬機器需要將位元組碼翻譯成機器碼。

程式碼如何在JVM執行?.jpg

從虛擬機器視角來看,執行Java程式碼首先需要將它編譯而成的 class 檔案載入到 Java 虛擬機器中。載入後的 Java 類會被存放於方法區(Method Area)中。實際執行時,虛擬機器會執行方法區內的程式碼。Java 虛擬機器同樣也在記憶體中劃分出堆和棧來儲存執行時資料。

不同的是,Java 虛擬機器會將棧細分為面向 Java 方法的 Java 方法棧,面向本地方法(用 C++ 寫的 native 方法)的本地方法棧,以及存放各個執行緒執行位置的 PC 暫存器。

Java虛擬機器-軟體層面.png

在執行過程中,每當呼叫進入一個 Java 方法,Java 虛擬機器會在當前執行緒的 Java 方法棧中生成一個棧幀,用以存放區域性變數以及位元組碼的運算元。這個棧幀的大小是提前計算好的,而且 Java 虛擬機器不要求棧幀在記憶體空間裡連續分佈。

當退出當前執行的方法時,不管是正常返回還是異常返回,Java 虛擬機器均會彈出當前執行緒的當前棧幀,並將之捨棄。

GraalVM帶來哪些神奇的黑魔法?‍♂️

  1. 更快、更輕量化的應用

    GraalVM的高效能JIT編譯器可以生成最佳化的本地機器程式碼,由於採用了先進的編譯器最佳化和積極複雜的內聯技術,執行速度更快,產生的垃圾更少,使用的CPU更少。最終的結果是應用程式執行速度更快,消耗的資源更少,從而降低了雲和基礎設施的成本。

  2. Ahead-Of-Time(AOT)提前編譯技術

    AOT 提前編譯,是相對於即時編譯而言的。AOT在執行過程中耗費 CPU 資源來進行即時編譯,而程式也能夠在啟動的瞬間就達到理想的效能。例如 C 和 C++語言採用的是AOT靜態編譯,直接將程式碼轉換成機器碼執行。而 Java 一直採用的是解釋 + 即時編譯技術。

    GraalVM 的 AOT 編譯實際上是藉助了 SubstrateVM 編譯框架,可以將 SubstrateVM 理解為一個內嵌精簡版的 JVM,包含異常處理,同步,執行緒管理,記憶體管理(垃圾回收)和 JNI 等元件。

    SubstrateVM 的啟動時間非常短,記憶體開銷非常少。用這種方式編譯出的 Java 程式的執行時間可與C語言持平。

  3. 語言互操作性

    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-list.png

GraalVM在 SDKMAN上有多個版本,可透過sdk list java進行檢視,我這裡使用的是22.2 社群版本

安裝SDKMAN!, 請看安裝文件

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提前編譯技術也不是完全沒有缺點,其對於多語言、反射和位元組碼動態載入技術需要相對繁瑣的配置。

native-image縮圖.png

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啟動程式碼,啟動時間如下:

啟動時間-IDEA.png

AOT提前編譯後native-image啟動時間:

➜  target git:(develop-v0.6.6) ✗ ./toolkit
......
startUp cost runTime: 11

佔用記憶體的效果

專案程式碼統計:

程式碼統計.png

編譯後可在linux、樹莓派、mac系統和windows系統可執行的映象;

native-image.png

上述的專案配置和落地實踐專案見: https://github.com/IoT-Technology/IoT-Toolkit

結束語

GraalVM對於JVM的開發人員來說,無疑是個非常好的訊息。但是留給JVM的時間真的不多了。

相關文章