0-07 秒啓動一個 SpringBoot 項目!

寫一段簡單的 Java 程序。

public class Hello {
    public static void main(String[] args) {
         System.out.println("hello world");
    }
}

[root@flash ~]# javac Hello.java
[root@flash ~]# java Hello
hello world

我們換一種方式來編譯這個程序,首先下載一個 GraalVM 的 native-image 工具,然後。

[root@flash ~]# native-image Hello
[hello:11725]    classlist:   1,031.19 ms,  0.96 GB
[hello:11725]        (cap):   2,624.14 ms,  0.96 GB
[hello:11725]        setup:   3,960.95 ms,  0.96 GB
[hello:11725]     (clinit):     288.49 ms,  1.72 GB
[hello:11725]   (typeflow):   2,642.38 ms,  1.72 GB
[hello:11725]    (objects):   3,803.54 ms,  1.72 GB
[hello:11725]   (features):   1,176.79 ms,  1.72 GB
[hello:11725]     analysis:   8,288.82 ms,  1.72 GB
[hello:11725]     universe:     909.14 ms,  1.75 GB
[hello:11725]      (parse):     801.67 ms,  1.75 GB
[hello:11725]     (inline):   1,096.07 ms,  2.32 GB
[hello:11725]    (compile):   7,352.50 ms,  2.37 GB
[hello:11725]      compile:  10,146.59 ms,  2.37 GB
[hello:11725]        image:   1,639.93 ms,  2.37 GB
[hello:11725]        write:     682.24 ms,  2.37 GB
[hello:11725]      [total]:  26,855.67 ms,  2.37 GB
# Printing build artifacts to: .../hello.build_artifacts.txt

執行完這個命令後,發現當前目錄多了個 hello 文件。

直接執行它,可以成功!

[root@flash ~]# ./hello
hello world

而且注意,這個是可以直接以二進制形式運行的,不依賴 jre。

也就是說,一個 Java 程序,被這個 native-image 編譯成了本地代碼!

這項技術來自於 GraalVM 的一個特性,在其官網的文檔中可以瞭解到,GraalVM 主要有三大特性:

  1. 通過新的 JIT 技術使 Java 程序更快運行

  2. 多語言支持

  3. 構建 JVM 無關的本地鏡像

這個 native-image 技術就是其中的第三點,即將 Java 代碼編譯成 JVM 無關的本地鏡像,使其可以直接以二進制的方式運行起來。

除了運行方便之外,我們對比一下這倆的文件大小啓動時間

[root@flash ~]# ll
-rw-r--r--  1 flash  staff   415B 10 27 15:50 Hello.class
-rwxr-xr-x  1 flash  staff    10M 10 27 15:51 hello

[root@flash ~]# time java Hello
hello world
java Hello  0.09s user 0.03s system 113% cpu 0.106 total

[root@flash ~]# time ./hello
hello world
./hello  0.00s user 0.01s system 34% cpu 0.032 total

總結個表格。

Hello.class  415B  0.12s

hello  10M  0.01s

可以看出,啓動時間大大縮短了!但文件大小卻大大增加了。

不過要知道,運行 Hello.class 要整個 jre 的支持,而運行二進制的 hello 卻不需要,這部分文件大小的差距,在小代碼上對比並不公平。

當然,啓動時間也都是毫秒級的,差距也不足以說明問題。下面我們試着用這種方式,對比一個 Spring Boot 項目。

有一點要說明的是,GraalVM 的本地編譯對 Java 代碼有很多的限制,有的時候需要配合配置文件才能成功,比如不支持動態類加載、反射、序列化等,具體可以見這裏:

https://www.GraalVM.org/reference-manual/native-image/Limitations/

可是 Spring 項目中可是大量充斥着這些,我們需要增加好多配置文件,才能成功本地編譯一個 Spring Boot 項目。

好在,Spring 已經爲我們考慮好這些事情了,提供了一個專門爲 native 而生的 Spring Boot 依賴項,最方便的是我們新建項目的時候可以直接從 start.spring.io 生成。

然後可以直接用 mvn 命令來打包一個本地鏡像。

[root@flash ~]# mvn package -Pnative
...
[INFO] Executing: ...native-image -cp ... -H:Name=demo-1
...
[demo-1:7725]    classlist:   1,695.81 ms,  0.94 GB
[demo-1:7725]        (cap):   1,932.48 ms,  0.94 GB
[demo-1:7725]        setup:   3,287.65 ms,  0.94 GB
[demo-1:7725]     (clinit):   2,256.61 ms,  5.68 GB
[demo-1:7725]   (typeflow):  18,462.41 ms,  5.68 GB
[demo-1:7725]    (objects):  17,848.47 ms,  5.68 GB
[demo-1:7725]   (features):   4,646.24 ms,  5.68 GB
[demo-1:7725]     analysis:  45,521.71 ms,  5.68 GB
[demo-1:7725]     universe:   2,624.03 ms,  5.68 GB
[demo-1:7725]      (parse):   1,917.71 ms,  5.68 GB
[demo-1:7725]     (inline):   6,021.71 ms,  5.93 GB
[demo-1:7725]    (compile):  30,497.99 ms,  6.06 GB
[demo-1:7725]      compile:  42,184.66 ms,  6.06 GB
[demo-1:7725]        image:   8,700.31 ms,  5.90 GB
[demo-1:7725]        write:   1,647.51 ms,  5.90 GB
[demo-1:7725]      [total]: 106,412.95 ms,  5.90 GB
# Printing build artifacts to: .../demo-1.build_artifacts.txt

同樣,我們用傳統的 jar 包方式打包一個 jar 文件,對比一下。

-rwxr-xr-x  1 flash  staff    66M Nov  2 16:11 demo-1
-rw-r--r--  1 flash  staff    17M Nov  2 16:09 demo-1-exec.jar

這回大小已經沒差那麼多了,但仍然是二進制的本地包大。不過這僅僅是幾乎空的 Spring Boot 項目,隨着項目依賴的包越來越多,二進制的文件大小會越來越有優勢,這是後話了。

我們再來對比一下啓動速度,首先是傳統的 jar 包運行。

[root@flash ~]# java -jar demo-1-exec.jar 


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '| '_| | '\/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.6)


2021-11-02 16:36:11.192  INFO 9468 --- [main] com.example.demo1.Demo1Application       : Starting Demo1Application v0.0.1-SNAPSHOT using Java 11.0.12 on sunyiming07deMacBook-Pro.local with PID 9468 (/Users/sunyiming07/IdeaProjects/graalvm-demos/springboot/demo/demo-1/target/demo-1-0.0.1-SNAPSHOT-exec.jar started by sunyiming07 in /Users/sunyiming07/IdeaProjects/graalvm-demos/springboot/demo/demo-1/target)
2021-11-02 16:36:11.195  INFO 9468 --- [main] com.example.demo1.Demo1Application       : No active profile set, falling back to default profiles: default
2021-11-02 16:36:12.097  INFO 9468 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-11-02 16:36:12.110  INFO 9468 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-11-02 16:36:12.110  INFO 9468 --- [main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.54]
2021-11-02 16:36:12.164  INFO 9468 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-11-02 16:36:12.164  INFO 9468 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 917 ms
2021-11-02 16:36:12.484  INFO 9468 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-11-02 16:36:12.494  INFO 9468 --- [main] com.example.demo1.Demo1Application       : Started Demo1Application in 2.033 seconds (JVM running for 2.504)

2.033 秒,已經慢下來了,不過正常的空 Spring Boot 項目也就這樣。

再看看本地鏡像啓動速度。

[root@flash ~]# ./demo-1
2021-11-02 16:38:33.141  INFO 9724 --- [main] o.s.nativex.NativeListener               : This application is bootstrapped with code generated with Spring AOT


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '| '_| | '\/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.5.6)


2021-11-02 16:38:33.143  INFO 9724 --- [main] com.example.demo1.Demo1Application       : Starting Demo1Application v0.0.1-SNAPSHOT using Java 11.0.12 on sunyiming07deMacBook-Pro.local with PID 9724 (/Users/sunyiming07/IdeaProjects/graalvm-demos/springboot/demo/demo-1/target/demo-1 started by sunyiming07 in /Users/sunyiming07/IdeaProjects/graalvm-demos/springboot/demo/demo-1/target)
2021-11-02 16:38:33.143  INFO 9724 --- [main] com.example.demo1.Demo1Application       : No active profile set, falling back to default profiles: default
2021-11-02 16:38:33.178  INFO 9724 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2021-11-02 16:38:33.178  INFO 9724 --- [main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2021-11-02 16:38:33.178  INFO 9724 --- [main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.54]
2021-11-02 16:38:33.184  INFO 9724 --- [main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2021-11-02 16:38:33.184  INFO 9724 --- [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 41 ms
2021-11-02 16:38:33.204  INFO 9724 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-11-02 16:38:33.204  INFO 9724 --- [main] com.example.demo1.Demo1Application       : Started Demo1Application in 0.078 seconds (JVM running for 0.08)
我去!0.078 秒!!!

我還真從來沒有啓動 Spring Boot 項目體驗過這麼極速的狀態呢!!!容我高興一會兒。

看吧,前面的 hello world 項目看不出什麼,現在的 Spring Boot 項目,優勢就已經完全出來了,啓動速度秒殺呀!

可想而知,我們原來啓動可能要幾分鐘才成功的 Spring Boot 項目,會被這個 GraalVM 優化到多少呢?想想就激動!

不過這個我還沒有試,光是跑這個 Spring Boot 空項目就忙活了好久,一直報各種各樣奇怪的錯誤,等我再熟練熟練的。

剛剛也說了,想通過 GraalVM 的 native-image 功能編譯一個 Java 程序,有很多限制,比如不支持動態類加載、反射、動態代理、JNI、序列化以及 invoke dynamic 指令等。

這是由於,AOT 這種提前編譯的技術,需要一個封閉空間假設,即在編譯期就能夠把運行期所有需要的東西都準備好,但 Java 的好多特性就是和這種封閉空間假設相沖突的。

Java 啓動後隨着程序不斷運行,JVM 將一部分代碼編譯成本地代碼,這個叫 JIT 技術,它是在程序運行起來之後不斷分析而做的編譯,所以它不受封閉空間假設的限制。

說回 GraalVM 的 AOT,比如程序中有個反射,這就屬於運行時纔會知道有這樣一個 Student 類被需要的情況。

Class.forName("com.flash.Student")

當然,GraalVM 會通過掃描這些反射方法的調用,來嘗試分析用到了哪些類。

如果分析不出來,就需要程序員手動配置,告訴 GraalVM 有哪些類要反射。

[
    {
        name: "com.flash.Student",
        allDeclaredConstructors: true,
        allPublicMethods: true
    },
    {
        name: "com.flash.Teacher",
        fileds: [{name: "teach"}{name: "talk"}],
        methods: [{
            name: "<init>",
            parameterTypes: ["char[]"]
        }]
    },
    // ……
]

但這樣肯定是反人性的。

自己寫的代碼和依賴還好,但如果是使用第三方組建,比如人人都用的 Spring,肯定不能由程序員來去寫這些配置文件。

那就只有讓 Spring 官方提供這些配置,讓程序員仍然是簡單寫一些 maven 依賴就能把項目跑起來,才能把這個技術推廣出去,這也是剛剛 Spring Native 項目存在的意義。

今天簡單給大家分享下 GraalVM 的使用,這個技術基本還沒有公司大規模在用,還達不到工業級的成熟,不過未來雲原生領域要求小包快速啓動兩個特性,GraalVM 的未來說不定有大舞臺呢。

我也纔剛剛開始玩這個東西,很多地方還沒搞懂,所以很希望能有一個 GraalVM 專項討論羣一起來學習和討論,不過一直沒找到。

既然找不到那就自己建一個吧!如果有了解這個技術的,或者想要了解這個技術的,可以加我好友備註【graalvm】拉你進羣,我們一起研討!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Hvi54yrbnfYl3S7j1rrcDw