JVM(十六:編譯器優化)

解釋器與編譯器

解釋器與編譯器兩者各有優勢:當程序需要迅速啟動和執行的時候,解釋器可以首先發揮作用,省去編譯的時間,立即運行。
當程序啟動后,隨著時間的推移,編譯器逐漸發揮作用,把越來越多的代碼編譯成本地代碼,這樣可以減少解釋器的中間損耗,獲得更高的執行效率。

當程序運行環境中內存資源限制較大,可以使用解釋執行節約內存,反之可以使用編譯執行來提升效率。
同時,解釋器還可以作為編譯器激進優化時后備的“逃生門”,讓編譯器根據概率選擇一些不能保證所有情況都正確,但大多數時候都能提升運行速度的優化手段,當激進優化的假設不成立,如加載了新類以后,類型繼承結構出現變化、出現“罕見陷阱”(Uncommon Trap)時可以通過逆優化(Deoptimization)退回到解釋狀態繼續執行,因此在整個Java虛擬機執行架構里,解釋器與編譯器經常是相輔相成地配合工作。



HotSpot虛擬機中內置了兩個(或三個)即時編譯器,其中有兩個編譯器存在已久,分別被稱為“客戶端編譯器”(Client Compiler)和“服務端編譯器”(Server Compiler),或者簡稱為C1編譯器和C2編譯器(部分資料和JDK源碼中C2也叫Opto編譯器),第三個是在JDK 10時才出現的、長期目標是代替C2的Graal編譯器。

在分層編譯(Tiered Compilation)的工作模式出現以前,HotSpot虛擬機通常是采用解釋器與其中一個編譯器直接搭配的方式工作,程序使用哪個編譯器,只取決于虛擬機運行的模式,HotSpot虛擬機會根據自身版本與宿主機器的硬件性能自動選擇運行模式,用戶也可以使用“-client”或“-server”參數去強制指定虛擬機運行在客戶端模式還是服務端模式。

無論采用的編譯器是客戶端編譯器還是服務端編譯器,解釋器與編譯器搭配使用的方式在虛擬機中被稱為“混合模式”(Mixed Mode),用戶也可以使用參數“-Xint”強制虛擬機運行于“解釋模式”(Interpreted Mode),這時候編譯器完全不介入工作,全部代碼都使用解釋方式執行。另外,也可以使用參數“-Xcomp”強制虛擬機運行于“編譯模式”(Compiled Mode),這時候將優先采用編譯方式執行程序,但是解釋器仍然要在編譯無法進行的情況下介入執行過程。

實施分層編譯后,解釋器、客戶端編譯器和服務端編譯器就會同時工作,熱點代碼都可能會被多次編譯,用客戶端編譯器獲取更高的編譯速度,用服務端編譯器來獲取更好的編譯質量,在解釋執行的時候也無須額外承擔收集性能監控信息的任務,而在服務端編譯器采用高復雜度的優化算法時,客戶端編譯器可先采用簡單優化來為它爭取更多的編譯時間。

棧上替換
對于函數體內多次循環執行的情況,編譯器會記錄循環的次數,達到一定的次數后虛擬機就會判定這段代碼為熱點代碼,然后對其即時編譯成機器碼。


盡管編譯動作是由循環體所觸發的,熱點只是方法的一部分,但編譯器依然必須以整個方法作為編譯對象,只是執行入口(從方法第幾條字節碼指令開始執行)會稍有不同,編譯時會傳入執行入口點字節碼序號。這種編譯方式因為編譯發生在方法執行的過程中,因此被很形象地稱為“棧上替換”,即方法的棧幀還在棧上,方法就被替換了。

逃逸分析

逃逸分析的基本原理是:分析對象動態作用域,當一個對象在方法里面被定義后,它可能被外部方法所引用,例如作為調用參數傳遞到其他方法中,這種稱為方法逃逸;甚至還有可能被外部線程訪問到,譬如賦值給可以在其他線程中訪問的實例變量,這種稱為線程逃逸;從不逃逸、方法逃逸到線程逃逸,稱為對象由低到高的不同逃逸程度。

如果能證明一個對象不會逃逸到方法或線程之外(換句話說是別的方法或線程無法通過任何途徑訪問到這個對象),或者逃逸程度比較低(只逃逸出方法而不會逃逸出線程),則可能為這個對象實例采取不同程度的優化。

棧上分配
在Java虛擬機中,Java堆上分配創建對象的內存空間幾乎是Java程序員都知道的常識,Java堆中的對象對于各個線程都是共享和可見的,只要持有這個對象的引用,就可以訪問到堆中存儲的對象數據。

虛擬機的垃圾收集子系統會回收堆中不再使用的對象,但回收動作無論是標記篩選出可回收對象,還是回收和整理內存,都需要耗費大量資源。如果確定一個對象不會逃逸出線程之外,那讓這個對象在棧上分配內存將會是一個很不錯的主意,對象所占用的內存空間就可以隨棧幀出棧而銷毀。

在一般應用中,完全不會逃逸的局部對象和不會逃逸出線程的對象所占的比例是很大的,如果能使用棧上分配,那大量的對象就會隨著方法的結束而自動銷毀了,垃圾收集子系統的壓力將會下降很多。棧上分配可以支持方法逃逸,但不能支持線程逃逸。

標量替換

若一個數據已經無法再分解成更小的數據來表示了,Java虛擬機中的原始數據類型(int、long等數值類型及reference類型等)都不能再進一步分解了,那么這些數據就可以被稱為標量。相對的,如果一個數據可以繼續分解,那它就被稱為聚合量(Aggregate),Java中的對象就是典型的聚合量。如果把一個Java對象拆散,根據程序訪問的情況,將其用到的成員變量恢復為原始類型來訪問,這個過程就稱為標量替換。

假如逃逸分析能夠證明一個對象不會被方法外部訪問,并且這個對象可以被拆散,那么程序真正執行的時候將可能不去創建這個對象,而改為直接創建它的若干個被這個方法使用的成員變量來代替。

同步消除

線程同步本身是一個相對耗時的過程,如果逃逸分析
能夠確定一個變量不會逃逸出線程,無法被其他線程訪問,那么這個變量的讀寫肯定就不會有競爭,
對這個變量實施的同步措施也就可以安全地消除掉。

摘抄:《深入理解Java虛擬機:JVM高級特性與最佳實踐》-第十一章

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容