一文讓你搞懂各種虛擬機、解釋器、JIT和AOT編譯器

問題提出

java開發者都執行,我們用java編寫的源碼會被javac編譯成字節碼,然後jvm執行字節碼就運行起來了。絕大多數初級java開發者對java程序的運行估計就理解到這個程度。其實,還有很多問題是要回答的?

  • 什麼是字節碼?爲啥要有字節碼的存在?不同VM的字節碼一樣嗎?
  • VM對字節碼是怎麼執行的?
  • VM執行引擎中的解釋器和編譯器有什麼不同?
  • JIT和AOT是什麼含義呢?

什麼是字節碼?爲啥要有字節碼的存在?

字節碼(byte code)是一種包含執行程序,由一系列OP代碼(操作碼)/數據對組成的二進制文件。字節碼是一種中間碼,它比機器碼更抽象,需要藉助虛擬機(VM)纔行執行。通常情況下字節碼是已經經過編譯的(這裏的編譯指的是前端編譯,後面會說說到),但與特定機器碼無關。字節碼通常不像源碼一樣可以讓人閱讀,而是編碼後數值常量、引用、指令等構成的序列。
字節碼主要爲了實現特定軟件運行和軟件環境,與硬件環境無關。字節碼的實現方式是通過編譯器和虛擬機,編譯器將源碼編譯成字節碼,特定平臺上的虛擬機通過執行引擎執行字節碼,後面會說到執行引擎是怎麼運作的。

不同VM的字節碼一樣嗎?

JVM也有很多種,就單Oracle來說就有JRockit和Hotspot兩款,JRockit號稱是“世界上最快的java虛擬機”,還有就是IBM的J9 VM等。
Android早期,google專門爲移動設備開發了一款虛擬機DalvikDalvik只能稱做“虛擬機”,而不能稱做“java虛擬機”,它沒有遵循java虛擬機規範,不能直接執行java的.class文件,使用的是寄存器架構而不是JVM常見的棧架構。我們知道Dalvik執行的是dex文件,通過dx轉換工具可以將JVM中的運行的字節碼(.class格式)轉換成Dalvik VM中運行的字節碼(.dex格式),所以,Dalvik的字節碼和JVM的字節碼是不一樣的。

VM對字節碼是怎麼執行的?

VM有java虛擬機也有Android虛擬機,它們對字節碼的執行是不一樣的,即便都是JVM,不同的java虛擬機對字節碼的執行也是不一樣的。
HotSpot VM採用解釋器+自適應編譯的執行引擎執行字節碼,具體看HotSpot VM,JRockit VM採用JIT編譯器+自適應編譯器的執行引擎執行字節碼,具體看JRockit VM

Dalvik和ART發展歷程

2008年9月,Android發佈,Dalvik VM的執行引擎是隻有解釋器的;
2010年5月,Android 2.2發佈,Dalvik VM引入了JIT編譯器,JIT的引入使得Dalvik的性能提升了3~6倍;
2013年10月,Android 4.4發佈,Dalvik和ART並存;
2014年10月,Android 5.0發佈,ART取代了Dalvik成爲了VM,同時AOT也成爲了唯一的編譯模式;單純的使用JIT和AOT都是有缺點的,具體看JIT編譯和AOT編譯比較
2016年8月,Android 7.0發佈,JIT編譯器迴歸,形成了AOT/JIT混合編譯模式,吸取了兩者的優點同時克服了缺點。

一些概念的解釋

hot spot

“hot spot”這個拼寫方式通常指比較寬泛的“熱點”概念。在執行引擎的上下文中,“熱點”指的是執行頻率到的代碼;至於“執行頻率高的代碼”是以什麼爲單位,是“方法/函數”級別還是“某條執行路徑(trace)”級別,都可以;這是實現者可以選擇的點。

HotSpot VM

HotSpot VM得名於它的混合模式執行引擎:這個執行引擎包含解釋器和自適應編譯器(adaptive compiler)。
默認配置下,一開始所有Java方法都是由解釋器執行。解釋器記錄着每個方法的調用次數和循環次數,並以這兩個數值爲指標判斷一個方法的“熱度”,顯然,HotSpot VM是以“方法”爲單位來尋找熱點代碼的。等到一個方法足夠“熱”的時候,HotSpot VM就會啓動對該方法的編譯。

自適應編譯(adaptive compilation)

在所有執行過的代碼裏只尋找一部分來編譯的做法,叫做自適應編譯。爲了實現自適應編譯,執行引擎通常需要有多層:至少要有一層能夠處理初始階段的執行,然後再自適應編譯其中部分代碼。

JIT編譯

JIT編譯,全稱just-in-time compilation,按照其原始的、嚴格的定義,是每當一部分代碼準備要第一次執行的時候,將這部分代碼編譯,然後跳進編譯好的代碼裏執行。這樣,所有執行過的代碼都必然會被編譯過。早期的JIT編譯系統對同一塊代碼只會編譯一次。JIT編譯的單元也可以選擇是方法/函數級別,或者別的,如trace。

動態編譯(dynamic compilation)

JIT編譯和自適應編譯都屬於動態編譯,或者叫運行時編譯的範疇,特點是在程序運行的時候進行編譯,而不是在程序開始運行之前就完成了編譯;後者叫做靜態編譯(static compilation)或AOT編譯(ahead-of-time compilation)

嚴格說JIT編譯與自適應編譯相比:

  • 前者的編譯時機比後者早:第一次執行之前 vs 已經被執行過若干次
  • 前者編譯的代碼比後者多:所有執行過的代碼 vs 一部分代碼

現在“JIT編譯”這個名稱已經被泛化爲等價於“動態編譯”,所以包含了嚴格的JIT編譯和自適應編譯。 也就是說,提到JIT編譯,它其實說的是動態編譯(運行時編譯),它具體指的可能是自適應編譯也可能是嚴格的JIT編譯。按照絕大多少文章的上下文,JIT編譯說的是根據熱點進行編譯的自適應編譯。
比如,上面提到的HotSpot VM中的自適應編譯在很多時候被叫做“JIT編譯”;裏面的Client Compiler(C1)和Server Compiler(C2)也常被稱爲“JIT編譯器”。

JRockit VM

JRockit VM使用純編譯的執行引擎,沒有解釋器。但它有多層編譯:第一次執行某個方法之前會用非常低的優化級別去JIT編譯,然後等到某個方法足夠熱之後再用較高的優化級別重新編譯它。這種系統既是嚴格意義上的JIT編譯(第一次執行某個方法前編譯它),又是自適應編譯(找出熱點再進行編譯)。
所以說JIT編譯與自適應編譯可以共存。只不過HotSpot VM因爲有解釋器來承擔第一層執行的任務,沒使用JIT編譯而已。

前端編譯

我們運行javac命令的過程,其實就是javac編譯器解析Java源代碼並生成字節碼文件的過程,這個過程就叫做前端編譯。 說白了,其實就是使用javac編譯器把Java語言規範轉化爲字節碼語言規範。
javac 編譯器的處理過程可以分爲下面四個階段:

  • 第一個階段:詞法、語法分析。在這個階段,JVM 會對源代碼的字符進行一次掃描,最終生成一個抽象的語法樹。簡單地說,在這個階段 JVM 會搞懂我們的代碼到底想要幹嘛。就像我們分析一個句子一樣,我們會對句子劃分主謂賓,弄清楚這個句子要表達的意思一樣。
  • 第二個階段:填充符號表。我們知道類之間是會互相引用的,但在編譯階段,我們無法確定其具體的地址,所以我們會使用一個符號來替代。在這個階段做的就是類似的事情,即對抽象的類或接口進行符號填充。等到類加載階段,JVM會將符號替換成具體的內存地址。
  • 第三個階段:註解處理。我們知道 Java 是支持註解的,因此在這個階段會對註解進行分析,根據註解的作用將其還原成具體的指令集。
  • 第四個階段:分析與字節碼生成。到了這個階段,JVM 便會根據上面幾個階段分析出來的結果,進行字節碼的生成,最終輸出爲 class 文件。

JIT編譯和AOT編譯比較

我們以Android平臺爲例,這裏說的JIT編譯是泛化的概念,具體指的是自適應編譯。
JIT和AOT的不同之處在於:JIT是在運行時進行編譯,是動態編譯,並且每次運行程序的時候都需要對odex重新進行編譯;而AOT是靜態編譯,應用在安裝的時候會啓動dex2oat把dex預編譯成ELF文件,每次運行程序的時候不用重新編譯,是真正意義上的本地應用。

JIT編譯模式的缺點:

  • 每次啓動應用都需要重新編譯;
  • 運行時比較耗電,造成電池額外的開銷;

AOT編譯模式的缺點:

  • 應用安裝和系統升級之後的應用優化比較耗時;
  • 優化後文件會佔用額外的存儲空間;

AOT的缺點就是JIT的優點,JIT的缺點也就是AOT的優點,即每次啓動應用和應用運行時不需要編譯,很快,同時也省電了。

ART VM中AOT/JIT混合編譯

應用在安裝的時候dex不會被編譯,在運行dex文件先通過解釋器(Interpreter)解釋執行(這一步驟跟Android2.2-Android4.4的行爲是一致的),與此同時,熱點函數(hot code)會被識別並被JIT編譯後存儲在jit code cache中並生成profile文件以記錄熱點函數的信息。手機進入IDLE(空閒)或者Charging(充電)狀態的時候,系統會掃描App目錄下的profile文件並執行AOT編譯。
也就說,應用在安裝的時候沒有編譯,所以安裝會很快,首次啓動和運行時還是採用解釋器+JIT編譯的模式,雖然也有慢和耗電問題,但是,被系統AOT編譯後,以後啓動和運行就很快了,也不耗電。

Dalvik VM

Dalvik虛擬機是Google等廠商合作開發的Android移動設備平臺的核心組成部分之一,它可以支持已轉換爲.dex格式的java應用程序的運行,.dex格式是專爲Dalvik設計的一種壓縮格式,適合內存和處理器速度有限的系統
Dalvik經過優化,允許在有限的內存中同時運行多個虛擬機實例,並且每個Dalvik應用做爲一個獨立的Linux進程執行。獨立的進程可以防止在虛擬機崩潰的時候所有程序都被關閉。
Dalvik早期是採用解釋器執行dex字節碼的,Android 2.2加入了JIT編譯器,採用瞭解釋器+JIT編譯的方式,雖然性能提升了,可是每次啓動應用都得動態編譯,效率還是不是很高,Android 4.4引入了ART VM採用AOT編譯模式(靜態編譯),5.0徹底拋棄了Dalvik,7.0採用了AOT+JIT混合編譯模式。

DVM(Dalvik VM)與JVM的區別

DVM之所以不是一個JVM,主要原因是DVM並沒有遵循JVM規範來實現,主要區別如下:

  • 基於的架構不同

JVM基於棧實現的,意味着需要去棧中讀寫數據,所需的指令會更多,這樣會導致速度慢,對於性能有限的移動設備,顯然不是很合適。
DVM是基於寄存器的,它沒有基於棧的虛擬機在拷貝數據而使用的大量的出入棧指令,同時指令更緊湊更簡潔;但是,由於顯示指定了操作數,所以基於寄存器的指令會比基於棧的指令要大,但是由於指令數量的減少,總的代碼數不會增加多少。

  • 執行的字節碼不同

在Java SE中,Java類會被編譯成一個或多個.class文件,打包成jar文件,而後JVM會通過相應的.class文件和jar文件獲取字節碼;執行順序爲:.java文件 -> .class文件 -> .jar文件。而DVM會用dx工具將所有的.class文件轉換爲一個.dex文件,然後DVM會從該.dex文件讀取指令和數據;執行順序爲:.java文件 -> .class文件 -> .dex文件。
.jar文件裏面包含多個.class文件,每個.class文件裏面包含了該類的常量池、類信息、屬性等等。當JVM加載該.jar文件的時候,會加載裏面的所有的.class文件,JVM的這種加載方式很慢,對於內存有限的移動設備並不合適。 而在.apk文件中只包含了一個.dex文件,這個.dex文件裏面將所有的.class裏面所包含的信息全部整合在一起了,這樣再加載就提高了速度。.class文件存在很多的冗餘信息,dex工具會去除冗餘信息,並把所有的.class文件整合到.dex文件中,減少了I/O操作,提高了類的查找速度。

通過上面的解釋,我們知道DVM爲了移動設備做了很多優化,這是因爲移動設備有內存和處理器速度有限的特點。首先,架構變成了基於寄存器的,相應的指令集也進行了變化,指令個數變少了,而且對內存的讀取變少了;然後,對由指令和數據組成的執行程序,也就是字節碼,進行了編碼和優化。

  • DVM允許在有限的內存中同時運行多個進程

DVM經過優化,允許在有限的內存中同時運行多個進程。在Android中的每一個應用都運行在一個DVM實例中,每一個DVM實例都運行在一個獨立的進程空間。獨立的進程可以防止在虛擬機崩潰的時候所有程序都被關閉。

難道JVM是一個進程運行多個應用的嗎?

  • DVM由Zygote創建和初始化

Zygote可以稱它爲孵化器,它是一個DVM進程,同時它也用來創建和初始化DVM實例。每當系統需要創建一個應用程序時,Zygote就會fork自身,快速地創建和初始化一個DVM實例,用於應用程序的運行。

  • DVM架構

DVM的源碼位於dalvik/目錄下,其中dalvik/vm目錄下的內容是DVM的具體實現部分,它會被編譯成 libdvm.so;dalvik/libdex會被編譯成libdex.a靜態庫,作爲dex工具使用;dalvik/dexdump是.dex文件的反編譯工具;DVM的可執行程序位於dalvik/dalvikvm中,將會被編譯成dalvikvm可執行程序。

  • DVM的運行時堆

DVM的運行時堆主要由兩個Space以及多個輔助數據結構組成,兩個Space分別是Zygote Space(Zygote Heap)Allocation Space(Active Heap)。Zygote Space用來管理Zygote進程在啓動過程中預加載和創建的各種對象,Zygote Space中不會觸發GC,所有進程都共享該區域,比如系統資源。Allocation Space是在Zygote進程fork第一個子進程之前創建的,它是一種私有進程,Zygote進程和fock的子進程在Allocation Space上進行對象分配和釋放。

除了這兩個Space,還包含以下數據結構:

Card Table: 用於DVM Concurrent GC,當第一次進行垃圾標記後,記錄垃圾信息。 Heap Bitmap: 有兩個Heap Bitmap,一個用來記錄上次GC存活的對象,另一個用來記錄這次GC存活的對象。 Mark Stack: DVM的運行時堆使用標記-清除(Mark-Sweep)算法進行GC,不瞭解標記-清除算法的同學查看Java虛擬機(四)垃圾收集算法這篇文章。Mark Stack就是在GC的標記階段使用的,它用來遍歷存活的對象。

參考

機器碼(machine code)和字節碼(byte code)是什麼?
JVM虛擬機種類
Dalvik和Java字節碼的對比
HotSpot是較新的Java虛擬機技術,用來代替JIT技術,那麼HotSpot和JIT是共存的嗎? - RednaxelaFX的回答 - 知乎
爲什麼 JVM 不用 JIT 全程編譯?
JVM基礎系列第4講:從源代碼到機器碼,發生了什麼?
Java 執行引擎(從字節碼到機器碼)
Dalvik 和 ART 有什麼區別?深扒 Android 虛擬機發展史,真相卻出乎意料!
Dalvik虛擬機和ART(Android RunTime)的區別
Android運行環境Dalvik模式和ART模式的區別
說說 Android 虛擬機Dalvik與ART區別在哪裏?
虛擬機隨談(一):解釋器,樹遍歷解釋器,基於棧與基於寄存器,大雜燴

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章