JVM結構與機制

yuziyi發表於2018-03-06

1、JVM是什麼

Java虛擬機器(Java Virtual Machine),一種能夠執行位元組碼的虛擬機器,將位元組碼解釋成不同os下的機器指令,有了jvm,java語言在不同平臺上執行時不需要重新編譯,即平臺無關性。

原理:編譯後的 Java 程式指令並不直接在硬體系統的 CPU 上執行,而是由 JVM 執行。JVM遮蔽了與具體平臺相關的資訊,使Java語言編譯程式只需要生成在JVM上執行的目標位元組碼(.class),滿足了高階語言要求的可移植性、可傳輸性、預編譯等等需求。

JVM結構與機制

1、以 Java 為例,我們在文字編譯器寫好了 Java 程式碼,交由「編譯器」編譯成 Java Bytecode。然後 Bytecode 交由 JVM 來執行,這時候 JVM 充當了「直譯器」的角色,在解釋 Bytecode 成 Machine Code 的同時執行它,返回結果。 2、以 BASIC 語言(早期的可以由計算機直譯的語言)為例,通過文字編譯器編寫好,不用經歷「編譯」的過程,就可以直接交由作業系統內部來進行「解釋」然後執行。 3、以 C 語言為例,我們在文字編譯器編寫好原始碼,然後執行 gcc hello.c 編譯出 hello.out 檔案,該檔案由一系列的機器指令組成的機器碼,可以直接交由硬體來執行。

1.2 jvm的啟動流程

JVM結構與機制

jvm.cfg
-server KNOWN
-client IGNORE
-hotspot ALIASED_TO -server
-classic WARN
-native ERROR
-green ERROR

KNOWN 表示存在 、IGNORE 表示不存在 、ALIASED_TO 表示給別的JVM去一個別名 WARN 表示不存在時找一個替代 、ERROR 表示不存在丟擲異常

1.3 jvm的優勢,其他語言選擇在JVM實現

目前有很多語言選擇了jvm,比如說Scala,Kotlin,Ceylon,Xtend,Groovy,Clojure;
JVM經過長期的發展,已經足夠成熟和完備。一個完整的語言包括 前端、優化、後端、runtime、庫 JVM 把後面四個都給包辦了。

  • 非常經濟地實現跨平臺。語言編譯器只需要輸出 JVM 位元組碼就可以。跨平臺需要極大的工作量,舉個例子,只是獨立開發生成原生程式碼,就需要花費大量精力去針對不同平臺和處理器進行優化 。
  • JVM 卓越的 JIT (Just-In-Time 即時編譯)效能。 JIT 可以在執行中記錄程式執行的特徵,並在其基礎上做大量的優化(Java 企業級應用的優秀效能很大程度上是由此而來)。 JIT 自從 HotSpot JVM 隨 Java 1.2 釋出以來,JVM JIT 的效能不斷提高,是無可爭議的成功產品。把 JVM 作為目標平臺意味著大量的效能優化工作可以「外包」給 JVM 來做,大大縮減了 Guest 語言的開發預算。
  • 已經有多個成熟的例項,有大量的經驗可以借鑑
  • JVM 作為一個成熟的高層執行環境,為 Guest 語言提供了很多執行時所需要的服務,比如記憶體管理(有業界領先的垃圾回收等),很大程度上避免了額外的獨立開發。
  • JVM 有多個獨立實現,也有若干廠商會持續推進,資料完備,社群巨大。
  • Java 社群有大量成熟的庫,一般來說,執行在 JVM 上的其它語言都會設計一個專用的「橋」來幫助直接使用 Java 的庫,對潛在客戶來說是個很好的賣點。
  • Java 有還算不錯的開發工具和環境。目標為 JVM 的很多語言會考慮用 Java 語言實現(至少在 bootstrap 階段)。

2、JVM結構

JVM結構與機制
JVM = 類載入器 classloader + 執行引擎 execution engine + 執行時資料區域 runtime data area

在Java虛擬機器的規範中定義了一系列的子系統、記憶體區域、資料型別和使用指南。這些元件構成了Java虛擬機器的內部結構,他們不僅僅為Java虛擬機器的實現提供了清晰的內部結構,更是嚴格規定了Java虛擬機器實現的外部行為。

這裡簡單介紹一下各個元件的作用

  • 類載入器子系統:每一個Java虛擬機器都由一個類載入器子系統(class loader subsystem),負責載入程式中的型別(類和介面),並賦予唯一的名字。每一個Java虛擬機器都有一個執行引擎(execution engine)負責執行被載入類中包含的指令。

  • 程式的執行需要一定的記憶體空間,如位元組碼、被載入類的其他額外資訊、程式中的物件、方法的引數、返回值、本地變數、處理的中間變數等等。Java虛擬機器將 這些資訊統統儲存在資料區data areas中。雖然每個Java虛擬機器的實現中都包含資料區,但是Java虛擬機器規範對資料區的規定卻非常的抽象。許多結構上的細節部分都留給了 Java虛擬機器實現者自己發揮。不同Java虛擬機器實現上的記憶體結構千差萬別。一部分實現可能佔用很多記憶體,而其他以下可能只佔用很少的記憶體;一些實現可 能會使用虛擬記憶體,而其他的則不使用。這種比較精煉的Java虛擬機器記憶體規約,可以使得Java虛擬機器可以在廣泛的平臺上被實現。

  • 資料區中的一部分是整個程式共有,其他部分被單獨的執行緒控制。每一個Java虛擬機器都包含方法區(method area)堆(heap),他們都被整個程式共享。Java虛擬機器載入並解析一個類以後,將從類檔案中解析出來的資訊儲存與方法區中。程式執行時建立的 物件都儲存在堆中。 當一個執行緒被建立時,會被分配只屬於他自己的PC暫存器“pc register”(程式計數器)和Java堆疊(Java stack)。當執行緒不掉用本地方法時,PC暫存器中儲存執行緒執行的下一條指令。Java堆疊儲存了一個執行緒呼叫方法時的狀態,包括本地變數、呼叫方法的 引數、返回值、處理的中間變數。呼叫本地方法時的狀態儲存在本地方法堆疊中(native method stacks),可能再暫存器或者其他非平臺獨立的記憶體中。

  • Java堆疊有堆疊塊(stack frames (or frames))組成。堆疊塊包含Java方法呼叫的狀態。當一個執行緒呼叫一個方法時,Java虛擬機器會將一個新的塊壓到Java堆疊中,當這個方法執行結束時,Java虛擬機器會將對應的塊彈出並拋棄。

  • Java虛擬機器不使用暫存器儲存計算的中間結果,而是用Java堆疊在存放中間結果。這是的Java虛擬機器的指令更緊湊,也更容易在一個沒有暫存器的裝置上實現Java虛擬機器。

2.1、直譯器

直譯器我們可以理解為,把一種高階語言轉換成另一種語言的程式。在JVM中,直譯器即將位元組碼檔案轉成機器二進位制語言,使我們的電腦可以直接執行。然而因為每次執行程式時都要先轉成另一種語言再作執行,因此直譯器的執行速度可想而知,這也造成了java執行速度比較慢的印象。

從本質上講,每個程式都是一臺機器的“描述”,而直譯器就是在“模擬”這臺機器的運轉,也就是在進行“計算”。所以從某種意義上講,直譯器就是計算的本質

直譯器一般都是“遞迴程式”。之所以是遞迴的原因,在於它處理的資料結構(程式)本身是“遞迴定義”的結構。

在java1.2版本之後,如今的HotSpot VM中不僅內建有直譯器,還內建有先進的JIT(Just In Time Compiler)編譯器,在Java虛擬機器執行時,直譯器和即時編譯器能夠相互協作,各自取長補短

2.2、JIT

JVM結構與機制

HotSpot虛擬機器採用了熱點程式碼探測技術:通過計數器找出最具編譯價值的程式碼,通知JIT以方法為單位進行編譯。如果方法被頻繁呼叫,則觸發標準編譯;如果方法中迴圈次數很多,觸發棧上替換編譯動作。 HotSpot無需等待原生程式碼輸出後才能執行程式,使得即時編譯壓力減小,有助於採用更更多的程式碼優化技術。輸出高質量的作業系統原生程式碼。

為什麼HotSpot虛擬機器要使用直譯器與編譯器並存的架構?

儘管並不是所有的Java虛擬機器都採用直譯器與編譯器並存的架構,但許多主流的商用虛擬機器(如HotSpot),都同時包含直譯器和編譯器。直譯器與編譯器兩者各有優勢:當程式需要迅速啟動和執行的時候,直譯器可以首先發揮作用,省去編譯的時間,立即執行。在程式執行後,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的程式碼編譯成原生程式碼之後,可以獲取更高的執行效率。當程式執行環境中記憶體資源限制較大(如部分嵌入式系統中),可以使用直譯器執行節約記憶體,反之可以使用編譯執行來提升效率。此外,如果編譯後出現“罕見陷阱”,可以通過逆優化退回到解釋執行。

編譯的時間開銷

直譯器的執行,抽象的看是這樣的:

輸入的程式碼 -> [ 直譯器 解釋執行 ] -> 執行結果

而要JIT編譯然後再執行的話,抽象的看則是:

輸入的程式碼 -> [ 編譯器 編譯 ] -> 編譯後的程式碼 -> [ 執行 ] -> 執行結果

說JIT比解釋快,其實說的是“執行編譯後的程式碼”比“直譯器解釋執行”要快,並不是說“編譯”這個動作比“解釋”這個動作快。 JIT編譯再怎麼快,至少也比解釋執行一次略慢一些,而要得到最後的執行結果還得再經過一個“執行編譯後的程式碼”的過程。 所以,對“只執行一次”的程式碼而言,解釋執行其實總是比JIT編譯執行要快。 怎麼算是“只執行一次的程式碼”呢?粗略說,下面兩個條件同時滿足時就是嚴格的“只執行一次”

1、只被呼叫一次,例如類的構造器(class initializer,())

2、沒有迴圈

對只執行一次的程式碼做JIT編譯再執行,可以說是得不償失。 對只執行少量次數的程式碼,JIT編譯帶來的執行速度的提升也未必能抵消掉最初編譯帶來的開銷。

只有對頻繁執行的程式碼,JIT編譯才能保證有正面的收益。 編譯的空間開銷 對一般的Java方法而言,編譯後程式碼的大小相對於位元組碼的大小,膨脹比達到10x是很正常的。同上面說的時間開銷一樣,這裡的空間開銷也是,只有對執行頻繁的程式碼才值得編譯,如果把所有程式碼都編譯則會顯著增加程式碼所佔空間,導致“程式碼爆炸”。 這也就解釋了為什麼有些JVM會選擇不總是做JIT編譯,而是選擇用直譯器+JIT編譯器的混合執行引擎。

為何HotSpot虛擬機器要實現兩個不同的即時編譯器?

HotSpot虛擬機器中內建了兩個即時編譯器:Client Complier和Server Complier,簡稱為C1、C2編譯器,分別用在客戶端和服務端。目前主流的HotSpot虛擬機器中預設是採用直譯器與其中一個編譯器直接配合的方式工作。程式使用哪個編譯器,取決於虛擬機器執行的模式。HotSpot虛擬機器會根據自身版本與宿主機器的硬體效能自動選擇執行模式。

用Client Complier獲取更高的編譯速度,用Server Complier 來獲取更好的編譯質量。為什麼提供多個即時編譯器與為什麼提供多個垃圾收集器類似,都是為了適應不同的應用場景。

開發人員可以通過如下命令顯式指定Java虛擬機器在執行時到底使用哪一種即時編譯器,如下所示:

-client:指定Java虛擬機器執行在Client模式下,並使用C1編譯器;

-server:指定Java虛擬機器執行在Server模式下,並使用C2編譯器。
複製程式碼

wiki上總結的jvm產品 https://en.wikipedia.org/wiki/Comparison_of_Java_virtual_machines

3、通過jclasslib檢視位元組碼檔案

JVM結構與機制

位元組碼轉義後可以容易的看出,是如何載入了init方法和add方法。

JVM結構與機制

iload_1 從區域性變數0中裝載int型別值
iload_2 從區域性變數0中裝載int型別值
iadd 執行int型別的加法
ireturn 從方法中返回int型別的資料
複製程式碼

1、[討論] [HotSpot VM] JIT編譯以及執行native http://hllvm.group.iteye.com/group/topic/39806

2、JVM編譯器的編譯過程 http://blog.csdn.net/tingfeng96/article/details/52261219

3、JVM即時編譯(JIT) http://blog.csdn.net/sunxianghuang/article/details/52094859

相關文章