1. 程式人生 > >深入理解Java虛擬機器讀書筆記7----晚期(執行期)優化

深入理解Java虛擬機器讀書筆記7----晚期(執行期)優化

七 晚期(執行期)優化
1 即時編譯器(JIT編譯器)
    ---當虛擬機發現某個方法或程式碼塊的執行特別頻繁時,就會把這些程式碼認定為“熱點程式碼”,包括被多次呼叫的方法和被多次執行的迴圈體。     ---為了提高熱點程式碼的執行效率,在執行時,虛擬機器就會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱為即時編譯器(JIT編譯器)。     ---即時編譯器不是虛擬機器必需的部分,虛擬機器規範對其沒有做任何規定,與具體的虛擬機器實現有關。   2 直譯器與編譯器

    ---許多主流的商用虛擬機器如HotSpot,都同時包含直譯器和編譯器。
    ---直譯器和編譯器兩者各有優勢:
        · 當程式需要迅速啟動和執行的時候。直譯器可以首先發揮作用,省去編譯的時間,立即執行;
        · 在程式執行後,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的程式碼編譯成原生代碼後,可以獲取更高的執行效率;
        ·
 當程式執行環境中記憶體資源限制較大,可以使用解釋執行節約記憶體,反之,可以使用編譯執行提升效率;
        · 直譯器還可以作為編譯器激進優化時的一個“逃生門”。
    ---HotSpot虛擬機器中內建了兩個即時編譯器:Client Compiler(C1編譯器)、Server Compiler(C2編譯器)。預設採用直譯器與其中一個編譯器直接配合的方式工作,程式使用哪個編譯器取決於虛擬機器執行的模式。
    ---分層編譯策略:
        ·
 第0層,程式解釋執行,直譯器不開啟效能監控功能,可觸發第1層編譯;
        · 第1層,也稱為C1編譯,將位元組碼編譯為原生代碼,進行簡單、可靠的優化,如有必要將加入效能監控的邏輯;
        · 第2層,也稱為C2編譯。將位元組碼編譯為原生代碼,會啟用一些編譯耗時較長的優化,甚至會根據效能監控資訊進行一些不可靠的激進優化。
  3 編譯物件和觸發條件     ---JIT編譯方式:方法被多次呼叫觸發的編譯,編譯器以整個方法作為編譯物件。
    ---棧上替換(OSR編譯):迴圈體被多次執行觸發的編譯,編譯器同樣以整個方法作為編譯物件。
    ---熱點探測:判斷一段程式碼是不是熱點程式碼,是不是需要觸發即時編譯。目前主要的判定方式有兩種:
        · 基於取樣的熱點探測:虛擬機器會週期性地檢查各個執行緒的棧頂,如果發現某個方法經常出現在棧頂,那這個方法就是“熱點方法”。
            ---優點:實現簡單、高效,可以很容易地獲取方法呼叫關係。
            ---缺點:很難精確地確認一個方法的熱度,容易因為受到執行緒阻塞或別的外界因素的影響而擾亂熱點探測。
        · 基於計數器的熱點探測:虛擬機器會為每個方法(甚至是程式碼塊)建立計數器,統計方法的執行次數,如果執行次數超過一定的閾值就認為它是“熱點方法”。
            ---優點:統計結果更加精確和嚴謹。
            ---缺點:實現比較複雜,不能直接獲取到方法呼叫關係。
    ---在HotSpot虛擬機器中使用的是基於計數器的熱點探測,它為每個方法準備了兩類計數器:方法呼叫計數器和回邊計數器。
    ---方法呼叫計數器:統計方法被呼叫的次數,在Client VM下,其觸發的即時編譯過程如下:
                             ---方法呼叫計數器熱度的衰減:當超過一定的時間限度,如果方法的呼叫次數仍然不足以讓它提交給即時編譯器編譯,那這個方法的呼叫計數器就會被減少到一半。一般是在虛擬機器進行垃圾收集時順便進行的。也可以設定為不進行熱度衰減。     ---回邊計數器:統計一個方法中迴圈體程式碼執行的次數,在位元組碼中遇到控制流向後跳轉的指令稱為“回邊”。     ---回邊計數器閾值計算公式:         · 虛擬機器執行在Client模式下,計算公式為:方法呼叫計數器閾值*OSR比率/100;         · 虛擬機器執行在Server模式下,計算公式為:方法呼叫計數器閾值*(OSR比率-直譯器監控比率)/100。     ---在Client VM下,回邊計數器觸發的即時編譯過程如下:                                  ---回邊計數器沒有熱度衰減的過程。當計數器溢位時,它還會把方法計數器的值也調整到溢位狀態,這樣下次再進入該方法的時候就會執行標準編譯過程(JIT編譯)。   4 編譯過程
    ---Server Compiler和Client Compiler兩個編譯器的編譯過程是不一樣的。
    ---Client Compiler主要的關注點在於區域性性的優化,而放棄了許多耗時較長的全域性優化手段,過程如下:
        · 一個平臺獨立的前端將字·節碼構造成一種高階中間程式碼表示(HIR),在此之前編譯器會在位元組碼上完成一部分基礎優化,如方法內聯、常量傳播等;
        · 一個平臺相關的後端從HIR中產生低階中間程式碼表示(LIR),在此之前編譯器會在HIR上完成另外一些優化,如空值檢查消除、範圍檢查消除等;
        · 在平臺相關的後端使用線性掃描演算法在LIR上分配暫存器,並在LIR上做窺孔優化,然後產生機器程式碼。
               5 編譯優化技術
    ---編譯方式執行原生代碼比解釋方式更快的原因:
        · 虛擬機器解釋執行位元組碼時需要額外的消耗時間;
        · 虛擬機器對程式碼的優化措施幾乎都集中在即時編譯器中。
    1)方法內聯
            ---主要目的是:
                · 去除方法呼叫的成本,如建立棧幀等;
                · 為其他優化建立良好的基礎,方法內聯膨脹之後可以便於在更大範圍上採取後續的優化手段,從而獲取更好的優化效果。
            ---Java虛擬機器的內聯過程:                 · 編譯器在進行方法內聯時,如果是非虛方法,那麼可以直接進行內聯。    
                · 如果是虛方法,則會向CHA查詢此方法在當前程式下是否有多個目標版本可供選擇,若查詢結果只有一個版本,那也可以進行內聯,但是這種內聯屬於激進優化,需要預留一個“逃生門”,稱為守護內聯。如果在程式的後續執行過程中,虛擬機器一直沒有載入到會令這個方法的接收者的繼承關係發生變化的類,那這個內聯優化的程式碼就可以一直使用下去。但如果載入了導致繼承關係發生變化的新類,那就需要拋棄已經編譯的程式碼,退回到解釋狀態執行,或者重新進行編譯。
                · 如果CHA查詢出來的結果是有多個版本的目標方法可供選擇,則編譯器會使用內聯快取來完成方法內聯。
            ---注1:型別繼承關係分析(CHA)技術:用於確定在目前已載入的類中,某個介面是否有多於一種的實現,某個類是否存在子類、子類是否為抽象類等資訊。
            ---注2:內聯快取工作原理:在未發生方法呼叫之前,內聯快取狀態為空,當第一次呼叫發生後,快取記錄下來方法接收者的版本資訊,並且每次進行方法呼叫時都比較接收者版本,如果以後進來的每次呼叫的方法接收者版本都是一樣的,那這個內聯還可以一直用下去。如果發生了方法接收者版本不一致的情況,就說明程式真正使用了虛方法的多型特性,這時會取消內聯,查詢虛方法表進行方法分派。
      (2)公共子表示式消除
            ---含義是:如果一個表示式E已經計算過了,並且從先前的計算到現在E中所有變數的值都沒有發生變化,那麼E的這次出現就成為了公共子表示式。對於這種表示式,沒有必要花時間再對它進行計算,只需要直接使用前面計算過的表示式結果代替E就可以了。
            ---語言無關的經典優化技術之一。
    (3)陣列邊界檢查消除
            ---語言相關的經典優化技術之一。
            ---把執行期檢查提到編譯期完成。
    (4)逃逸分析
            ---逃逸分析的基本行為就是分析物件動態作用域:當一個物件在方法中被定義後,它可能被外部方法所引用,稱為方法逃逸;甚至還有可能被外部執行緒訪問到,稱為執行緒逃逸。
            ---如果一個物件不會逃逸到方法或者執行緒之外,則可能為該變數進行如下優化:
                · 棧上分配:如果一個物件不會逃逸出方法之外,那麼可以讓這個物件在棧上分配記憶體。
                · 同步消除:如果一個物件不會逃逸出執行緒之外,那麼可以將對這個物件的同步措施消除掉。
                · 標量替換:如果逃逸分析證明一個物件不會被外部訪問,並且這個物件可以被拆散的話,那程式真正執行的時候將可能不建立這個物件,而改為直接建立它的若干個被這個方法使用到的成員變數來代替。
            ---注1:標量:一個數據已經無法再分解成更小的資料來表示,如Java虛擬機器中的原始資料型別(int、long等數值型別及reference型別等)。
            ---注2:聚合量:一個數據可以繼續分解,如Java中的物件。