瞎掰JAVA:執行引擎(一)

一 指令編譯

 

javac編譯器完成了詞法分析、語法分析以及抽象語法樹的過程,最終遍歷語法樹生成線性字節碼指令流的過程,此過程發生在虛擬機外部,也常常被叫做前端編譯器。

二 指令執行

 

解釋執行:將編譯好的字節碼一行一行地翻譯爲機器碼執行。通過解釋器來讀取字節碼,遇到相應的指令就去執行該指令。

編譯執行:以方法爲單位,將字節碼一次性翻譯爲機器碼後執行。將字節碼轉爲本地機器碼來執行;現代JVM會根據代碼熱點來生成相應的本地機器碼。


前者的優勢在於不用等待,後者則在實際運行當中效率更高。
爲了滿足不同的場景,HotSpot虛擬機內置了多個即時編譯器:C1,C2。

C1:即Client編譯器,面向對啓動性能有要求的客戶端GUI程序,採用的優化手段比較簡單,因此編譯的時間較短。

C2:即Server編譯器,面向對性能峯值有要求的服務端程序,採用的優化手段複雜,因此編譯時間長,但是在運行過程中性能更好。

  從Java7開始,HotSpot虛擬機默認採用分層編譯的方式:熱點方法首先被C1編譯器編譯,而後 熱點方法中的熱點再進一步被C2編譯(理解爲二次編譯,根據前面的運行計算出更優的編譯優化)。爲了不干擾程序的正常運行,JIT編譯時放在額外的線程中執行的,HotSpot根據實際CPU的資源,以1:2的比例分配給C1和C2線程數。在計算機資源充足的情況,字節碼的解釋運行和編譯運行時可以同時進行,編譯執行完後的機器碼會在下次調用該方法時啓動,已替換原本的解釋執行(意思就是已經翻譯出效率更高的機器碼,自然替換原來的相對低效率執行的方法)。


JIT編譯:
    Java 程序最初是僅僅通過解釋器解釋執行的,即對字節碼逐條解釋執行,這種方式的執行速度相對會比較慢,尤其當某個方法或代碼塊運行的特別頻繁時,這種方式的執行效率就顯得很低。於是後來在虛擬機中引入了 JIT 編譯器(即時編譯器),當虛擬機發現某個方法或代碼塊運行特別頻繁時,就會把這些代碼認定爲“Hot Spot Code”(熱點代碼),爲了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各層次的優化,完成這項任務的正是 JIT 編譯器。

現在主流的商用虛擬機(如Sun HotSpot、IBM J9)中幾乎都同時包含解釋器和編譯器(三大商用虛擬機之一的 JRockit 是個例外,它內部沒有解釋器,因此會有啓動相應時間長之類的缺點,但它主要是面向服務端的應用,這類應用一般不會重點關注啓動時間)。二者各有優勢:當程序需要迅速啓動和執行時,解釋器可以首先發揮作用,省去編譯的時間,立即執行;當程序運行後,隨着時間的推移,編譯器逐漸會返回作用,把越來越多的代碼編譯成本地代碼後,可以獲取更高的執行效率。解釋執行可以節約內存,而編譯執行可以提升效率。

HotSpot 虛擬機中內置了兩個JIT編譯器:Client Complier 和 Server Complier,分別用在客戶端和服務端,目前主流的 HotSpot 虛擬機中默認是採用解釋器與其中一個編譯器直接配合的方式工作。

運行過程中會被即時編譯器編譯的“熱點代碼”有兩類:

1.被多次調用的方法。
2.被多次調用的循環體。


兩種情況,編譯器都是以整個方法作爲編譯對象,這種編譯也是虛擬機中標準的編譯方式。要知道一段代碼或方法是不是熱點代碼,是不是需要觸發即時編譯,需要進行 Hot Spot Detection(熱點探測)。目前主要的熱點 判定方式有以下兩種:基於採樣的熱點探測:採用這種方法的虛擬機會週期性地檢查各個線程的棧頂,如果發現某些方法經常出現在棧頂,那這段方法代碼就是“熱點代碼”。這種探測方法的好處是實現簡單高效,還可以很容易地獲取方法調用關係,缺點是很難精確地確認一個方法的熱度,容易因爲受到線程阻塞或別的外界因素的影響而擾亂熱點探測。
基於計數器的熱點探測:採用這種方法的虛擬機會爲每個方法,甚至是代碼塊建立計數器,統計方法的執行次數,如果執行次數超過一定的閥值,就認爲它是“熱點方法”。這種統計方法實現複雜一些,需要爲每個方法建立並維護計數器,而且不能直接獲取到方法的調用關係,但是它的統計結果相對更加精確嚴謹。

在 HotSpot 虛擬機中使用的是第二種——基於計數器的熱點探測方法,因此它爲每個方法準備了兩個計數器:方法調用計數器和回邊計數器。

方法調用計數器用來統計方法調用的次數,在默認設置下,方法調用計數器統計的並不是方法被調用的絕對次數,而是一個相對的執行頻率,即一段時間內方法被調用的次數。

回邊計數器用於統計一個方法中循環體代碼執行的次數(準確地說,應該是回邊的次數,因爲並非所有的循環都是回邊),在字節碼中遇到控制流向後跳轉的指令就稱爲“回邊”。

在確定虛擬機運行參數的前提下,這兩個計數器都有一個確定的閥值,當計數器的值超過了閥值,就會觸發JIT編譯。觸發了 JIT 編譯後,在默認設置下,執行引擎並不會同步等待編譯請求完成,而是繼續進入解釋器按照解釋方式執行字節碼,直到提交的請求被編譯器編譯完成爲止(編譯工作在後臺線程中進行)。當編譯工作完成後,下一次調用該方法或代碼時,就會使用已編譯的版本。

三 指令環境

基於棧的指令集與基於寄存器的指令集直接的關係
1、JVM執行指令時所採取的方式是基於棧的指令集
2、基於棧的指令集主要的操作有入棧與出棧兩種。
3、基於棧的指令集的優勢在於它可以在不同平臺之間進行移植,而基於寄存器的指令集是與硬件架構緊密關聯的,無法做到可移植。
4、基於棧的指令集的缺點在於完成相同的操作,指令數量通常要比基於寄存器的指令集數量要多;基於棧的指令集是在內存中完成操作的,
而基於寄存器的指令集是直接由CPU來執行的,它是在高速緩衝區進行執行的,速度要快很多。雖然虛擬機可以採用一些優化手段,
但總體來說,基於棧的指令集的執行速度要慢一些。

 

 

四 瞎掰

我們討論一種場景在JAVA線程最終會系統調用的系統的線程,線程的狀態切換會引起系統用戶態和內核態的切換,導致系統性能損耗。在使用JAVA程序中調用native本地方法中同樣會發生C態(個人的稱呼)與JAVA態(JVM執行引擎執行)的切換,這個過程中成本究竟有多大?

發佈了8 篇原創文章 · 獲贊 0 · 訪問量 1989
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章