2018年4月,Oracle Labs新公開了一項黑科技:Graal VM。java
這是一個在HotSpot虛擬機基礎上加強而成的跨語言全棧虛擬機,能夠做爲「任何語言」的運行平臺使用。python
如今網絡上關於 Graal VM 的相關資料並很少,仍是要看官方文檔。本文旨在簡要介紹:git
本地應用程序
下面是一張 Graal VM 的簡要思惟導圖
。github
一篇通俗易懂的文章:GraalVM:微服務時代的Java。web
Graal VM 被官方稱爲「Universal VM」和「Polyglot VM」,是一個在HotSpot虛擬機基礎上加強而成的跨語言全棧虛擬機,口號是「Run Programs Faster Anywhere」。能夠在 Graal VM 上運行「任何語言」,這些語言包括:spring
Graal VM能夠無額外開銷地混合使用這些編程語言,支持不一樣語言中混用對方的接口和對象,也可以支持這些語言使用已經編寫好的本地庫文件。編程
具體可參考官方文檔:Why GraalVM?ruby
我認爲最重要的特性是 Ahead-of-Time Compilation
。Substrate VM 是一個在 Graal VM 0.20 版本里的極小型的運行時環境,包括了獨立的異常處理、同步調度、線程管理、內存管理(垃圾收集)和JNI訪問等組件。Substrate VM 還包含了一個本地鏡像的構造器
(Native Image Generator),用戶能夠經過本地鏡像構造器構建基於構建機器的可執行文件。bash
構造器採用指針分析(Points-To Analysis)技術,從用戶提供的程序入口出發,搜索全部可達的代碼。在搜索的同時,它還將執行初始化代碼,並在最終生成可執行文件時,將已初始化的堆保存至一個堆快照之中。網絡
Substrate VM就能夠直接從目標程序開始運行,而無須重複進行Java虛擬機的初始化過程。但相應地,原理上也決定了Substrate VM必需要求目標程序是徹底封閉的,即不能動態加載其餘編譯期不可知的代碼和類庫。基於這個假設,Substrate VM才能探索整個編譯空間,並經過靜態分析推算出全部虛方法調用的目標方法。
以往單個服務須要 7*24 小時不間斷運行,須要單機高可用,此時 Java 服務就很適合。可是 Java 應用程序都須要運行在上百兆的 JRE 上,在微服務上就並不合適。
同時在微服務中,應用能夠隨時拆分,每一個應用並不須要很大的內存,而是須要快速啓動、隨時更新,也可能不須要長時間運行。Java 應用程序原本啓動就很慢,同時須要充分預熱纔可以獲取高性能。
GraalVM 提早編譯就提供了一種解決方案,官方給出使用了 GraalVm 後啓動時間可以提升 50 倍,內存有 5 倍的降低。
Java 語言在微服務天生就有劣勢,這是由於 Java 誕生之初的口號就是「一次編寫,處處運行」。這個口號已經植入 Java 的基因中。若是想改變這些(真的要拿Java的劣勢去和別的語言的優點相比),會有不少困難:
Graal VM的基本工做原理是將這些語言的源代碼(例如JavaScript)或源代碼編譯後的中間格式(例如LLVM字節碼)經過解釋器轉換爲能被Graal VM接受的中間表示(Intermediate Representation,IR),譬如設計一個解釋器專門對LLVM輸出的字節碼進行轉換來支持C和C++語言,這個過程稱爲「程序特化」(Specialized,也常稱爲Partial Evaluation)。
Graal VM提供了Truffle工具集來快速構建面向一種新語言的解釋器,並用它構建了一個稱爲Sulong的高性能LLVM字節碼解釋器。
Linux、Windows 等其餘平臺能夠參考 Install GraalVM。因爲我使用 macOS,本篇文章介紹如何在 macOS 上安裝 Graal VM,基於 OpenJDK 11 的 GraalVM Community Edition。
macOS 上的 GraalVM 社區版是 tar.gz 文件,JDK 的安裝目錄是:
/Library/Java/JavaVirtualMachines/<graalvm>/Contents/Home
x86 64位的 macOS 安裝步驟以下:
` 下載。
tar -xvf graalvm-ce-java11-darwin-amd64-20.1.0.tar.gz
/Library/Java/JavaVirtualMachines
目錄下(須要使用 sudo)。sudo mv graalvm-ce-java11-20.1.0 /Library/Java/JavaVirtualMachines
檢測是否安裝成功,能夠運行命令:
/usr/libexec/java_home -V
運行結果爲:
Matching Java Virtual Machines (2): 11.0.7, x86_64: "GraalVM CE 20.1.0" /Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home 1.8.0_201, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home /Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home
將 GraalVM bin
目錄加入 PATH
環境變量。
export PATH=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home/bin:$PATH
設置 JAVA_HOME
環境變量。
export JAVA_HOME=/Library/Java/JavaVirtualMachines/graalvm-ce-java11-20.1.0/Contents/Home
注意:可能須要修改 bashc 配置文件。
經過上述步驟,已經安裝好了 GraalVM 的基礎組件,若是須要額外支持 Python、R 等語言,須要使用 gu
組件。
gu install ruby gu install r gu install python gu install wasm
安裝 GraalVM Native Image
,運行命令:
gu install native-image
安裝 LLVM toolchain
組件,運行命令:
gu install llvm-toolchain
能夠參考 GitHub 的 spring-boot-graalvm 項目,這個項目裏詳細列出了 GraalVM 編譯 Spring Boot Java 應用程序可能出現的全部問題,並對比了 Java 應用啓動與編譯成本地可執行的 Java 程序。
Spring與Graal VM共同維護的在Spring Graal Native項目已經提供了大多數Spring Boot組件的配置信息(以及一些須要在代碼層面處理的Patch),咱們只須要簡單依賴該工程便可。這樣 Graal VM 就能獲取編譯期的反射、動態代理等配置。咱們只須要簡單依賴工程便可。
須要在 pom.xml 中增長依賴:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-indexer</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.experimental</groupId> <artifactId>spring-graalvm-native</artifactId> <version>0.7.1</version> </dependency>
指定啓動類的路徑:
<properties> <start-class>com.yano.workflow.WorkflowApplication</start-class> </properties>
配置一個獨立的 profile,在編譯時經過 native-image-maven-plugin
插件將其編譯成本地可執行文件。
<profiles> <profile> <id>native</id> <build> <plugins> <plugin> <groupId>org.graalvm.nativeimage</groupId> <artifactId>native-image-maven-plugin</artifactId> <version>20.1.0</version> <configuration> <buildArgs>-J-Xmx4G -H:+TraceClassInitialization -H:+ReportExceptionStackTraces -Dspring.graal.remove-unused-autoconfig=true -Dspring.graal.remove-yaml-support=true </buildArgs> <imageName>${project.artifactId}</imageName> </configuration> <executions> <execution> <goals> <goal>native-image</goal> </goals> <phase>package</phase> </execution> </executions> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </profile> </profiles>
該插件在 Maven 中央倉庫不存在,須要指定 pluginRepositories 和 repositories:
<repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </pluginRepository> </pluginRepositories>
Graal VM不支持CGLIB,只能使用JDK動態代理,因此應當把Spring對普通類的Bean加強給關閉掉。Spring Boot 的版本要大於等於 2.2,SpringBootApplication
註解上將 proxyBeanMethods 參數設置爲 false。
@SpringBootApplication(proxyBeanMethods = false) public class SpringBootHelloApplication { public static void main(String[] args) { SpringApplication.run(SpringBootHelloApplication.class, args); } }
在命令行經過 maven 打包項目:
mvn -Pnative clean package
最終在 target 目錄可以看到可執行文件,大概在 50M 左右,相比 fat jar 爲 17M。
java -jar target/spring-boot-graal-0.0.1-SNAPSHOT.jar . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.0.M4) 2020-04-30 15:40:21.187 INFO 40149 --- [ main] i.j.s.SpringBootHelloApplication : Starting SpringBootHelloApplication v0.0.1-SNAPSHOT on PikeBook.fritz.box with PID 40149 (/Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target/spring-boot-graal-0.0.1-SNAPSHOT.jar started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm) 2020-04-30 15:40:21.190 INFO 40149 --- [ main] i.j.s.SpringBootHelloApplication : No active profile set, falling back to default profiles: default 2020-04-30 15:40:22.280 INFO 40149 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080 2020-04-30 15:40:22.288 INFO 40149 --- [ main] i.j.s.SpringBootHelloApplication : Started SpringBootHelloApplication in 1.47 seconds (JVM running for 1.924)
可以經過命令行直接運行程序,啓動速度賊快。對比 Hello World web 普通應用程序,啓動時間是 1.47s
,佔用內存 491 MB
。
而編譯成本地代碼的 Spring Boot 程序,啓動速度是 0.078s
,佔用內存 30 MB
。
./spring-boot-graal . ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: 2020-05-01 10:25:31.200 INFO 42231 --- [ main] i.j.s.SpringBootHelloApplication : Starting SpringBootHelloApplication on PikeBook.fritz.box with PID 42231 (/Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target/native-image/spring-boot-graal started by jonashecht in /Users/jonashecht/dev/spring-boot/spring-boot-graalvm/target/native-image) 2020-05-01 10:25:31.200 INFO 42231 --- [ main] i.j.s.SpringBootHelloApplication : No active profile set, falling back to default profiles: default 2020-05-01 10:25:31.241 WARN 42231 --- [ main] io.netty.channel.DefaultChannelId : Failed to find the current process ID from ''; using a random value: 635087100 2020-05-01 10:25:31.245 INFO 42231 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080 2020-05-01 10:25:31.245 INFO 42231 --- [ main] i.j.s.SpringBootHelloApplication : Started SpringBootHelloApplication in 0.078 seconds (JVM running for 0.08)
coding 筆記、點滴記錄,之後的文章也會同步到公衆號(Coding Insight)中,但願你們關注^_^
代碼和思惟導圖在 GitHub 項目中,歡迎你們 star!