談即時編譯優化-以異常堆棧丟失爲例

前言

日照充足會讓西瓜更甜,那擁有即時編譯優化會讓Java程序怎麼樣?本文會初步介紹JVM的即時編譯優化特性,而且經過異常堆棧丟失這一常見的現象來進行舉例java

即時編譯優化

Java程序在運行初期是經過解釋器來執行,當發現某塊代碼運行特別頻繁,就會將之斷定爲熱點代碼(Hot Spot Code), 虛擬機會將這部分代碼編譯成本地機器碼,並對這些代碼進行優化。這件事就是即時編譯(Just In Time, JIT)優化, 作這件事的就是即時編譯器緩存

解釋器與編譯器

目前主流虛擬機都採用解釋器、編譯器並存的架構。架構

  • 解釋器:程序執行初期,解釋器執行的方式能夠省去編譯過程,節省時間
  • 編譯器:在渡過初期後,編譯器把更多的代碼編譯成本地代碼,提高執行效率,以空間換時間

由於編譯器存在過分優化基於假設優化等失敗的優化結果,經過逆優化(Deoptimization)的方式,將程序的執行主動權從編譯器交給解釋器執行。能夠把解釋器當作是一個保守派,編譯器是一個激進派,在JVM執行體系裏,二者相輔相成,互相配合。ide

編譯器種類

通常虛擬機都內置了兩個或三個即時編譯器,歷史比較久遠的C1, C2, 以及在JDK10纔出現的Graal模塊化

  • C1:客戶端編譯器(Client Complier),執行時間較短,啓動程序的時間較快。在一些物聯網小型設備上可指定這種編譯器,經過-client參數強制指定性能

  • C2:服務端編譯器(Server Complier),執行時間較長,啓動時間較長但可編譯高度優化的代碼,峯值性能更高。可經過-server參數強制指定優化

  • Graal:是一個實驗性質的即時編譯器,其最大的特色是該編譯器用Java語言編寫,更加模塊化,也更容易開發與維護。充分預熱後Java代碼編譯成二進制碼後其執行性能並不亞於由C++編寫的C2。能夠經過參數 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 啓用,並替換 C2ui

分層編譯優化

雖然能夠經過-Xint參數強制虛擬機處於"解釋模式"此時編譯器不工做,能夠經過-Xcomp參數強制虛擬機處於"編譯模式"此時解釋器不工做,能夠經過-client參數使C2不工做,也能夠經過-server參數使C1不工做,可是並不推薦這樣作,由於有分層編譯優化這一特性。this

編譯器在編譯代碼的時候會佔用程序運行時間,優化程度越高的代碼編譯時間會越長,甚至會須要解釋器負責收集程序運行監控信息提供給編譯器來編譯優化程度更高的代碼。因此爲了在更短的時間內編譯優化程度更高的代碼,須要編譯器之間的配合,也就是所謂的分層編譯優化。一共有五層,分別是:spa

  1. 純解釋執行,解釋器不開啓收集程序運行監控信息

  2. 使用C1編譯器進行簡單可靠的優化,解釋器不開啓收集程序運行監控信息

  3. 仍然使用C1編譯器優化,可是會針對方法調用次數和回邊次數(循環代碼調用次數)相關的統計

  4. 仍然使用C1編譯器優化,統計信息才上一層的基礎上會加上分支跳轉、虛方法調用等所有統計信息,解釋器火力全開

  5. 使用C2編譯器優化,相比C1,C2會開啓更多耗時更長的優化,還會根據解釋器提供的程序運行信息進行一些更爲激進的優化

在開啓編譯優化後,熱點代碼可能會被重複編譯,C1編譯器編譯得更快,C2編譯器編譯質量更高,第0層模式解釋器執行的時候也不用收集監控信息,第4層模式C2在進行耗時較長的編譯較爲忙碌時候,C1也能爲C2承擔一部分編譯工做,交互關係以下圖

  • common是針對大部分代碼的編譯狀況,trival method針對執行次數較少的代碼
  • trival method不多被執行因此沒有被C2編譯的必要,經過第4層模式的優化就足夠了
  • 在C1忙碌的時候,會直接由C2編譯;C2忙碌的時候,在C1編譯的路徑也會更長

編譯觸發條件

上面提到即便編譯是針對熱點代碼進行編譯優化,那麼什麼是熱點代碼?

  1. 被屢次調用的方法
  2. 被屢次執行的循環代碼體

這裏的屢次如何知道具體有多少次?有兩種方法能夠知道

  • 基於採樣的熱點探測(Sample Based Hot Spot Code Detection): 虛擬機週期性地檢查各個線程的調用棧頂,若是發現某個方法常常出如今棧頂,那麼這個方法就是熱點方法,這種方法簡單高效可是精確度不高
  • 基於計數器的熱點探測(Counter Based Hot Spot Code Dection): 虛擬機爲每一個方法創建計數器,計數器超過必定閾值就是熱點方法

目前HotSpot虛擬機使用的是第二種方法,虛擬機爲每一個方法都準備了兩類計數器,方法調用計數器以及回邊計數器(回邊的意思是在循環的末尾邊界往回跳轉,能夠理解爲循環代碼的一次執行)

講到這裏給你們舉一個工做中常常見到的一個JIT優化案例:異常堆棧丟失

異常堆棧丟失

問題

衆所周知在打印Java異常的時候,會將其堆棧信息一併輸出,這些堆棧信息很是重要,有助於咱們排查問題,像這樣

20:10:50.491 [main] ERROR com.yangkw.ErrorTest
java.lang.NullPointerException: null
	at com.yangkw.ErrorTest.error(ErrorTest.java:33)
	at com.yangkw.ErrorTest.main(ErrorTest.java:19)
複製代碼

可是在最近在觀察系統的線上運行日誌的時候,發現了不少不帶堆棧的異常日誌,讓人摸不着頭腦到底發生了什麼,像這樣

20:10:50.491 [main] ERROR com.yangkw.ErrorTest
java.lang.NullPointerException: null
複製代碼

猜測

經過前面關於JIT編譯觸發條件的介紹,能夠設想是拋出異常執行太頻繁因此觸發了JIT優化致使,因而咱們能夠寫一個Demo來驗證,堆棧完整的時候打印"full trace",堆棧丟失的時候打印"no trace"

public static void main(String[] args) throws InterruptedException {
        int count = 0;
        while (true) {
            try {
                count++; //統計調用次數
                error();
            } catch (Exception e) {
                if (e.getStackTrace().length == 0) {
                    LOG.error("no trace count:{}", count, e);
                    Thread.sleep(1000); //方便觀察日誌
                } else {
                    LOG.error("full trace count:{}", count, e);
                }
            }
        }
    }
    private static void error() {
        String nullMsg = null;
        nullMsg.toString();
    }
複製代碼

下面是執行結果,能夠看出程序是在執行到8405次(每次執行都會不一樣)的時候丟失了堆棧

驗證

雖然8405次執行的時候丟失了堆棧,可是並不能說明是由於JIT優化致使的,因而咱們能夠加上參數-XX:+PrintCompilation 來打印即時編譯狀況。

能夠看到,在10388次執行的時候是有堆棧信息的,在10389次執行的時候就丟失了堆棧信息,在這中間就發生了即便編譯優化,針對這一現象官方術語稱之爲"fast throw"能夠經過參數-XX:-OmitStackTraceInFastThrow關閉這一優化

在ORACLE官方文檔有這麼一段描述

The compiler in the server VM now provides correct stack backtraces for all "cold" built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow.

堆棧丟失只是表面現象,JIT還對其作了如下優化:

  1. 建立須要拋出異常的實例
  2. 清空堆棧信息
  3. 將該實例緩存起來
  4. 以後再須要拋出的時候,將緩存實例拋出去

總結

  1. 解釋器、C1編譯器、C2編譯器各有優劣,合理搭配,幹活不累
  2. -XX:-OmitStackTraceInFastThrow 謹慎使用,若是關閉fast throw的優化應預防"日誌風暴"使磁盤空間迅速被打滿
  3. 作好歷史日誌的記錄以及備份,筆者經過回查歷史日誌成功追回了異常的堆棧信息
  4. 日照充足的西瓜會更甜,擁有即時編譯優化會讓Java程序程序更靈性
相關文章
相關標籤/搜索