你瞭解JVM中的 JIT 即時編譯及優化技術嗎?

leozzy發表於2018-07-01

JVM Client 模式和 Server模式的區別

你瞭解JVM中的 JIT 即時編譯及優化技術嗎?

通過 java -version 可檢視 JVM 所處的模式,並可以通過修改配置檔案進行配置,那它們有什麼區別呢?

Server:-Server 模式啟動時,速度較慢,但是啟動之後,效能更高,適合執行伺服器後臺程式

Client:-Client 模式啟動時,速度較快,啟動之後不如 Server,適合用於桌面等有介面的程式

熱點程式碼

理解

當虛擬機器發現某個方法或程式碼塊的執行特別頻繁時,就會把這些程式碼認定為“熱點程式碼”。

熱點程式碼的分類

  • 被多次呼叫的方法
一個方法被呼叫得多了,方法體內程式碼執行的次數自然就多,成為“熱點程式碼”是理所當然的。
  • 被多次執行的迴圈體

一個方法只被呼叫過一次或少量的幾次,但是方法體內部存在迴圈次數較多的迴圈體,這樣迴圈體的程式碼也被重複執行多次,因此這些程式碼也應該認為是“熱點程式碼”。

上面提到的多次是一個不具體的詞語,那到底是多少次才能成為熱點程式碼呢?

如何檢測熱點程式碼

判斷一段程式碼是否是熱點程式碼,是否需要觸發即使編譯,這樣的行為稱為熱點探測,熱點探測並不一定知道方法具體被呼叫了多少次,目前主要的熱點探測判定方式有兩種:

  • 基於取樣的熱點探測:採用這種方法的虛擬機器會週期性地檢查各個執行緒的棧頂如果發現某個(或某些)方法經常出現在棧頂,那這個方法就是“熱點方法”
優點:實現簡單高效,容易獲取方法呼叫關係(將呼叫堆疊展開即可)

缺點:不精確,容易因為因為受到執行緒阻塞或別的外界因素的影響而擾亂熱點探測

  • 基於計數器的熱點探測:採用這種方法的虛擬機器會為每個方法(甚至是程式碼塊)建立計數器,統計方法的執行次數,如果次數超過一定的閾值就認為它是“熱點方法”
優點:統計結果精確嚴謹

缺點:實現麻煩,需要為每個方法建立並維護計數器,不能直接獲取到方法的呼叫關係

HotSpot使用第二種 - 基於計數器的熱點探測方法。

確定了檢測熱點程式碼的方式,如何計算具體的次數呢?

計數器的種類(兩種共同協作)

  • 方法呼叫計數器:這個計數器用於統計方法被呼叫的次數。預設閾值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次
  • 回邊計數器:統計一個方法中迴圈體程式碼執行的次數
瞭解了熱點程式碼和計數器有什麼用呢?達到計數器的閾值會觸發後文講解的即時編譯,也就是說即時編譯是需要達到某種條件才會觸發的,先寫結論,後文講解什麼是即時編譯器。

兩個計數器的協作(這裡討論的是方法呼叫計數器的情況):當一個方法被呼叫時,會先檢查該方法是否存在被 JIT(後文講解) 編譯過的版本,如果存在,則優先使用編譯後的原生程式碼來執行。如果不存在已被編譯過的版本,則將此方法的呼叫計數器加 1,然後判斷方法呼叫計數器與回邊計數器之和是否超過方法呼叫計數器的閾值。如果已經超過閾值,那麼將會向即時編譯器提交一個該方法的程式碼編譯請求。

當編譯工作完成之後,這個方法的呼叫入口地址就會被系統自動改成新的,下一次呼叫該方法時就會使用已編譯的版本。

什麼是位元組碼、機器碼、原生程式碼?

位元組碼是指平常所瞭解的 .class 檔案,Java 程式碼通過 javac 命令編譯成位元組碼

機器碼和原生程式碼都是指機器可以直接識別執行的程式碼,也就是機器指令

位元組碼是不能直接執行的,需要經過 JVM 解釋或編譯成機器碼才能執行

此時你要問了,為什麼 Java 不直接編譯成機器碼,這樣不是更快嗎?

1. 機器碼是與平臺相關的,也就是作業系統相關,不同作業系統能識別的機器碼不同,如果編譯成機器碼那豈不是和 C、C++差不多了,不能跨平臺,Java 就沒有那響亮的口號 “一次編譯,到處執行”;

2.之所以不一次性全部編譯,是因為有一些程式碼只執行一次,沒必要編譯,直接解釋執行就可以。而那些“熱點”程式碼,反覆解釋執行肯定很慢,JVM 在執行程式的過程中不斷優化,用JIT編譯器編譯那些熱點程式碼,讓他們不用每次都逐句解釋執行;

3.還有一方面的原因是後文講解的直譯器與編譯器共存的原因。

什麼是 JIT ?

為了提高熱點程式碼的執行效率,在執行時,虛擬機器將會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器

什麼是編譯和解釋?

編譯器:把源程式的每一條語句都編譯成機器語言,並儲存成二進位制檔案,這樣執行時計算機可以直接以機器語言來執行此程式,速度很快;

直譯器:只在執行程式時,才一條一條的解釋成機器語言給計算機來執行,所以執行速度是不如編譯後的程式執行的快的;

通過javac命令將 Java 程式的原始碼編譯成 Java 位元組碼,即我們常說的 class 檔案。這是我們通常意義上理解的編譯。

位元組碼並不是機器語言,要想讓機器能夠執行,還需要把位元組碼翻譯成機器指令。這個過程是Java 虛擬機器做的,這個過程也叫編譯。是更深層次的編譯。(實際上就是解釋,引入 JIT 之後也存在編譯)

此時又有疑惑了,Java 不是解釋執行的嗎?

沒錯,Java 需要將位元組碼逐條翻譯成對應的機器指令並且執行,這就是傳統的 JVM 的直譯器的功能,正是由於直譯器逐條翻譯並執行這個過程的效率低,引入了 JIT 即時編譯技術。

必須指出的是,不管是解釋執行,還是編譯執行,最終執行的程式碼單元都是可直接在真實機器上執行的機器碼,或稱為原生程式碼

附一張圖來理解

你瞭解JVM中的 JIT 即時編譯及優化技術嗎?

編譯原理參考:[深入分析Java的編譯原理](www.hollischuang.com/archives/23…)

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

直譯器與編譯器兩者各有優勢

直譯器:當程式需要迅速啟動和執行的時候,直譯器可以首先發揮作用,省去編譯的時間,立即執行。

編譯器:在程式執行後,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的程式碼編譯成原生程式碼之後,可以獲取更高的執行效率。

兩者的協作:在程式執行環境中記憶體資源限制較大時,可以使用解釋執行節約記憶體,反之可以使用編譯執行來提升效率。當通過編譯器優化時,發現並沒有起到優化作用,,可以通過逆優化退回到解釋狀態繼續執行。

即時編譯器與 Java 虛擬機器的關係

即時編譯器並不是虛擬機器必需的部分,Java 虛擬機器規範並沒有規定 Java 虛擬機器內必須要有即時編譯器的存在,更沒有限定或指導即時編譯器應該如何去實現。

但是,即時編譯器編譯效能的好壞、程式碼優化程度的高低卻是衡量一款商用虛擬機器優秀與否的最關鍵的指標之一。它也是虛擬機器中最核心且最能體現虛擬機器技術水平的部分。

即時編譯器的分類

  • Client Compiler - C1編譯器
  • Server Compiler - C2編譯器
目前主流的 HotSpot 虛擬機器(JDK1.7 及之前版本的虛擬機器)預設採用一個直譯器和其中一個編譯器直接配合的方式工作,程式使用哪個編譯器,取決於虛擬機器執行的模式,就是文章開頭提到的兩種模式。


在 HotSpot 中,直譯器和 JIT 即時編譯器是同時存在的,他們是 JVM 的兩個元件。對於不同型別的應用程式,使用者可以根據自身的特點和需求,靈活選擇是基於直譯器執行還是基於 JIT 編譯器執行。HotSpot 為使用者提供了幾種執行模式供選擇,可通過引數設定,分別為:解釋模式、編譯模式、混合模式,HotSpot 預設是混合模式,需要注意的是編譯模式並不是完全通過 JIT 進行編譯,只是優先採用編譯方式執行程式,但是直譯器仍然要在編譯無法進行的情況下介入執行過程。

分層編譯

產生的原因:由於即時編譯器編譯原生程式碼需要佔用程式執行時間,要編譯出優化程度更高的程式碼,所花費的時間可能更長;而且要想編譯出優化程度更高的程式碼,直譯器可能還要替編譯器收集效能監控資訊,這對解釋執行的速度也有影響。為了在程式啟動響應速度與執行效率之間達到最佳平衡,HotSpot 虛擬機器啟用分層編譯的策略

分層編譯根據編譯器編譯、優化的規模與耗時,劃分出不同的編譯層次:

  • 第 0 層:程式解釋執行,直譯器不開啟效能監控功能,可觸發第 1 層編譯。
  • 第 1 層:也稱為 C1 編譯,將位元組碼編譯為原生程式碼,進行簡單,可靠的優化,如有必要將加入效能監控的邏輯。
  • 第 2 層(或 2 層以上):也稱為 C2 編譯,也是將位元組碼編譯為原生程式碼,但是會啟用一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化。
實施分層編譯後,Client Compiler 和 Server Compiler 將會同時工作,許多程式碼都可能會被多次編譯看,用 Client Compiler 獲取更高的編譯速度,用 Server Compiler 獲取更好的編譯質量,在解釋執行的時候也無須再承擔收集效能監控資訊的任務。


編譯優化技術

Java 程式設計師有一個共識,以編譯方式執行原生程式碼比解釋執行方式更快,之所以有這樣的共識,除去虛擬機器解釋執行位元組碼時額外消耗時間的原因外,還有一個重要的原因就是虛擬機器設計團隊幾乎把對程式碼的所有優化措施都集中在了即時編譯器中,因此一般來說,即時編譯器產生的原生程式碼會比 javac 產生的位元組碼更優秀。以下是具有代表性的 HotSpot 虛擬機器的即時編譯器在生成程式碼時採用的程式碼優化技術:

  • 語言無關的經典優化技術之一:公共子表示式消除
如果一個表示式 E 已經計算過了,並且從先前的計算到現在 E 中所有變數的值都沒有發生變化,那麼 E 的這次出現就成為了公共子表示式。對於這種表示式,沒必要花時間再對它進行計算,只需要直接使用前面計算過的表示式結果代替 E 就可以了。例子:int d = (c*b) * 12 + a + (a+ b * c) -> int d = E * 12 + a + (a+ E)

  • 語言相關的經典優化技術之一:陣列範圍檢查消除
在 Java 語言中訪問陣列元素的時候系統將會自動進行上下界的範圍檢查,超出邊界會丟擲異常。對於虛擬機器的執行子系統來說,每次陣列元素的讀寫都帶有一次隱含的條件判定操作,對於擁有大量陣列訪問的程式程式碼,這無疑是一種效能負擔。Java 在編譯期根據資料流分析可以判定範圍進而消除上下界檢查,節省多次的條件判斷操作。
  • 最重要的優化技術之一:方法內聯
簡單的理解為把目標方法的程式碼“複製”到發起呼叫的方法中,消除一些無用的程式碼。只是實際的 JVM 中的內聯過程很複雜,在此不分析。
  • 最前沿的優化技術之一:逃逸分析
逃逸分析的基本行為就是分析物件動態作用域:當一個物件在方法中杯定義後,它可能被外部方法所引用,例如作為呼叫引數傳遞到其他方法中,稱為方法逃逸。甚至可能被外部執行緒訪問到,譬如賦值給類變數或可以在其他執行緒中訪問的例項變數,稱為執行緒逃逸。

如果能證明一個物件不會逃逸到方法或執行緒之外,也就是別的方法或執行緒無法通過任何途徑訪問到這個物件,則可以為這個變數進行一些高效的優化:

  • 棧上分配:將不會逃逸的區域性物件分配到棧上,那物件就會隨著方法的結束而自動銷燬,減少垃圾收集系統的壓力。
  • 同步消除:如果該變數不會發生執行緒逃逸,也就是無法被其他執行緒訪問,那麼對這個變數的讀寫就不存在競爭,可以將同步措施消除掉(同步是需要付出代價的)
  • 標量替換:標量是指無法在分解的資料型別,比如原始資料型別以及reference型別。而聚合量就是可繼續分解的,比如 Java 中的物件。標量替換如果一個物件不會被外部訪問,並且物件可以被拆散的話,真正執行時可能不建立這個物件,而是直接建立它的若干個被這個方法使用到的成員變數來代替。這種方式不僅可以讓物件的成員變數在棧上分配和讀寫,還可以為後後續進一步的優化手段建立條件。


按自己理解整理的,知識點順序不知是否合適,還請大家指導。


參考來源:

《深入理解 Java 虛擬機器》

www.hollischuang.com/archives/23…








相關文章