作者:蔡不菜丶
juejin.im/post/5eba3190e51d454dd9407247
參考書籍:《Java性能權威指南》
作為Java開發人員,也許在工作中最經常用到的隻是 CRUD,解決性能問題 也許不經常接觸到,但是也是需要了解一二的!這篇文章小菜帶你一起探究
Java性能優化之JIT編譯器
。
文章大綱
前情概覽
即時
JIT(JUst-In-Time)
編譯器是Java虛拟機的核心,對 JVM性能 影響最大的也就是編譯器。
CPU
是計算機的核心,到時隻能執行相對少而且特定的指令,例如彙編碼和二進制碼 ,是以 CPU 所執行的程式都必須翻譯成這種指令。
語言結構
- 編譯型語言:會編譯成二進制形式 傳遞,先寫程式,然後用編譯器靜态生成二進制檔案。
- 解釋型語言:隻要機器上有合适的解釋器,相同的程式代碼可以在任何 CPU 上執行,執行程式時,解釋器會将相應代碼轉換為二進制代碼。
Java試圖走一條中間路線,Java應用會被編譯——但不是編譯成特定 CPU 所專用的二進制編碼,而是被編譯成一種理想化的彙編語言。然後該彙編語言(Java位元組碼)可以用Java運作。是以 Java 是一門 平台獨立性的解釋型語言。
熱點編譯
JVM 執行代碼時,隻會編譯經常被調用的。是以被編譯的代碼需要具備以下特性:
- 代碼是經常被調用的代碼
- 運作很多次疊代的循環
而這些關鍵代碼段被稱為應用的熱點,代碼執行得越多就被認為是越熱的。是以編譯器
會先解釋執行代碼,然後找出哪個方法被調用的足夠頻繁,才進行編譯
。這也是為了優化:JVM 執行特定方法或者循環的次數越多,它就會越了解這段代碼,這樣可以使 JVM 在編譯代碼時進行大量優化。
小結
- Java的設計結合了腳本語言的平台獨立性和編譯型語言的本地性能
- Java檔案被編譯成中間語言(Java位元組碼),然後在運作時被JVM進一步編譯成彙編語言
- 位元組碼編譯成彙編語言的過程中有大量的優化,極大地改善了性能
調優入門
一、兩種 “口味”
- Client 編譯器
- Server編譯器
指令行上選擇編譯器類型則采用以上兩個名字:
-client
和
-server
。通常這兩個編譯器也稱為 c1 編譯器(client編譯器) 和 c2 編譯器(server編譯器)
分層編譯器:分層編譯意味着必須使用 server 編譯器
關閉分層編譯:
java -client -XX:+TieredCompilation other_args
兩者的主要差別:
在于編譯代碼的時機不同。client編譯器開啟編譯比server編譯器要早。這意味着;在代碼執行的開始階段,client編譯器比server編譯器要快,因為他編譯代碼相比server編譯器而言要多。
問題來了:
JVM 能不能在啟動的時候用 client 編譯器,然後随着代碼變熱使用 server 編譯器?
方案:
分層編譯:代碼先由 client 編譯器編譯,随着代碼變熱, 由 server 編譯器重新編譯。在 Java 1.8 中,分層編譯是預設開啟的。
二、優化啟動
當快速啟動時間是首要目标時了,最常使用 client 編譯器。
當整體性能比啟動性能更重要時,更适合使用 server 編譯器。
小結:
- 如果應用的啟動時間是首要的性能考量,那 client 編譯器就是最有用的。
- 分層編譯的啟動時間可以非常接近于 client 編譯器所獲得的啟動時間。
三、優化批處理
處理過程
歸根結底取決于哪種編譯器使得應用運作的時間最優。
- 分層編譯是批處理任務合理的預設選擇
- 分層編譯是适合所有情況的很好的備選方案
- 分層編譯總是比标準的 server 編譯器好一些
- 即便應用永遠運作,server 編譯器也不可能編譯它的所有代碼
- 對于計算量固定的任務來說,應該選擇實際執行任務最快的編譯器
四、優化長時間運作的應用
通常來說,在應用 “熱身” 之後,意味着它已經運作了足夠長的時間,重要的代碼都已經被編譯,這個時候便可以測試它處理的吞吐量。一個應用測試結果:
對于長時間運作的應用來說,應該一直使用 server 編譯器,最好配合分層編譯。相比單獨的 server 編譯器,分層編譯可以編譯更多代碼,提供更多的性能。
編譯器中級調優
大多數情況下,所謂編譯器調優,其實就隻是為目标機器上的 Java 選擇正确的 JVM和編譯器開關(
-client
-server
-XX:+TieredCompilation
)而已。分層編譯通常是長期運作應用的最佳選擇,而對于運作時間短的應用來說,分層編譯與 client 編譯器的性能差别也微乎其微。
一、代碼緩存
JVM 編譯代碼時,會在代碼緩存中保留編譯之後的彙編語言指令集。代碼緩存的大小固定,是以一旦填滿,JVM 就不能編譯更多代碼了。代碼緩存過小會導緻隻有部分熱點編譯,而應用的大部分代碼都隻是解釋運作 —>
運作慢
代碼緩存填滿時,JVM會發出以下警告:
JAVA HotSpot(TM) 64-Bit Server VM warning:CodeCache is full
Compiler has been disabled
JAVA HotSpot(TM) 64-Bit Server VM warning:Try increasing the
code cache size using -XX:ReservedCodeCacheSize=
複制代碼
設定代碼緩存最大值
-XX:ReservedCodeCacheSize=N
設定代碼緩存初始大小
-XX:InitialCodeCacheSize=N
小結
- 代碼緩存是一種有最大值的資源,它會影響 JVM 可運作的編譯代碼總量。
- 分層編譯很容易達到代碼緩存預設配置的上限(特别是在 Java 7)。使用分層編譯時,應該監控代碼緩存,必要時應該增加它的大小。
二、編譯門檻值
編譯門檻值和 代碼執行的頻度 有關,一旦代碼執行達到一定次數,并且達到了編譯門檻值,編譯器就可以獲得足夠多的資訊來進行代碼的編譯。
編譯是基于兩種 JVM 計數器
- 方法調用計數器
- 方法中的循環回邊計數器(回邊可以看做是循環完成執行的次數,所謂循環完成執行,包括達到循環自身的末尾,也包括執行了像 continue 這樣的分支語句)
1. 标準編譯
JVM 執行了某個 Java 方法時,會檢查該方法的兩種計數器總數,然後判定該方法是否适合編譯,如果适合,該方法就會進入編譯隊列。
更改編譯門檻值
由
-XX:CompileThreshold=N
标志觸發,使用 client 編譯器時,N 的預設值是 1500,使用 server 編譯器時,N 的預設值是 10 000,更改 CompileThreshold 将會使編譯器提高(或延遲)編譯。
問題:
如果循環很長或者永遠不會退出,怎麼計數?
這種情況下,JVM 不等方法調用完成就會編譯循環,是以循環每完成一輪,回邊計數器就會增加并被檢測。
2. 棧上編譯 (OSR)
由于僅僅編譯循環還不夠,JVM 必須在循環進行的時候還能編譯循環,在循環代碼編譯結束後,JVM 就會替換還在棧上的代碼,循環的下一次疊代就會執行快得多的編譯代碼。
實際上會出現有些重要的方法永遠不會被編譯。因為并不是還沒達到編譯門檻值,而是永遠都達不到編譯門檻值!
這是因為雖然計數器随着方法和循環的執行而增加,但是它們也會随時間而減少。這種方法也稱為
溫熱方法
小結:
- 當方法和循環執行次數達到某個門檻值的時候,就會發生編譯
- 改變門檻值會導緻代碼提早或推後編譯
- 由于計數器會随着時間而減少,以至于 "溫熱方法" 可能永遠都打不到編譯的門檻值(特别是對 server 編譯器來說)
三、編譯過程
如果我們想要看到編譯器是如何工作的,可以使用
-XX:+PrintCompilation
指令來開啟,預設是 false
如果程式啟動時沒有開啟這個标志,可以用
jstat
了解編譯器内部的部分工作情況。例如:
jstat -compiler 25
或
jstat -printcompilation 25 1000
-
:提供了關于多少方法被編譯的概要資訊-compiler
-
:擷取最近被編譯的方法-printcompilation
-
:是被檢測程序的 ID25
-
:每 1 秒(1000毫秒)輸出一次1000
小結:
- 觀察代碼如何被編譯的最好方法是開啟 PrintCompilation
- PrintCompilation 開啟後所輸出的資訊可用來确認編譯是否和預期一樣
編譯器進階調優
一、編譯線程
當方法(或循環)适合編譯時,就會進入到編譯隊列。然後隊列中的任務則由一個或多個背景線程處理,這意味着編譯過程是異步的。這樣的好處便是:即便代碼正在編譯的時候,程式也能持續執行。
如果是用标準編譯所編譯的方法,那下次調用該方法時就會執行編譯後的方法;如果是用OSR編譯的循環,那下次循環疊代時就會執行編譯後的代碼。
編譯隊列并不嚴格遵守先進先出的原則:調用計數次數多的方法有更高的優先級。是以即便在程式開始執行并有大量代碼需要編譯時,這樣的優先順序仍然有助于確定最重要的代碼優先編譯。
使用client編譯器時,JVM會開啟一個編譯線程;使用server編譯器時,則會開啟兩個線程。而分層編譯器則是一個略複雜的等式而定,如下:
小結:
- 放置在編譯隊列中的方法的編譯會被異步執行。
- 隊列并不是嚴格按照先後順序的;隊列中的熱點方法會在其他方法之前編譯,這是編譯輸出日志中的 ID 為亂序的另一個原因。
二、内聯 可好?
有了解過final的小夥伴應該都知道被final修飾的方法,編譯時JVM會嘗試找與其内聯的方法。這是因為編譯器所做的最重要的優化是
方法内聯
。内聯預設是開啟的。可以通過
-XX:-Inline
關閉。
如果你從源代碼編譯 JVM,那可以用
-XX:+PrintInling
生成帶調試資訊的版本。方法是否 内聯 取決于它有多熱以及它的大小。JVM 依據内部計算來判定方法是否熱點(譬如:調用很頻繁);是否是熱點并不直接與任何調優參數相關。
小結:
- 内聯是編譯器所能做的最有利的優化,特别是對屬性封裝良好的面向對象的代碼來說。
- 幾乎用不着調節内聯參數,且提倡這樣做的建議往往忽略了正常内聯和頻繁調用内聯之間的關系。當考察内聯效應時,確定考慮這兩種情況。
三、逃逸分析
我們可以通過
-XX:+DoEscapeAnalysis
來開啟逃逸分析,預設是true。逃逸分析可以決定哪些優化是可能的,并決定編譯後的代碼中哪些是必要的改變。
逃逸分析預設開啟,極少數情況下,它會出錯。在此類情況下關閉它會變得更快或更穩定。如果你發現了這種情況,最好的應對行為就是簡化相關代碼:代碼越簡單越好。
小結:
- 逃逸分析是編譯器能做得最複雜的優化,此類優化常常會導緻微基準測試失敗。
- 逃逸分析常常會給不正确的同步代碼引入 bug。
何為逆優化
逆優化意味着編譯器不得不 “撤銷” 之前的某些編譯;結果是應用的性能降低——至少是直到編譯器重新編譯相應代碼為止。有兩種逆優化的情形:
- 代碼被丢棄(made not entrant)
- 産生僵屍代碼(made zombie)
一、代碼被丢棄了?
有兩種原因導緻代碼被丢棄
- 與類與接口的工作方式有關
- 與分層編譯的細節有關
當server編譯器編譯好代碼之後,JVM 必須替換 client 編譯器所編譯的代碼。它會将老弟阿瑪标記為廢棄。也用同樣的方法替換新編譯(和更有效)的代碼。
二、“僵屍” 代碼出現
何為僵屍代碼:當編譯後的代碼,因為後續沒有用到而被GC回收,全部回收之後,編譯器就會注意到,這些代碼現在适合标記為僵屍代碼了。
從性能角度上看,這是好事。上面我們提到過代碼緩存,編譯後的代碼會儲存在大小固定的代碼緩存中。如果發現僵屍代碼,這意味着這些有問題的代碼可以從代碼緩存中移除,騰出空間給其他将被編譯的代碼(或者限制 JVM 之後需要配置設定的記憶體量)。
可能産生不足的是,如果代碼被僵屍化以後再次加載并且頻繁使用,JVM 就需要重新編譯和重新優化代碼,那麼這将會影響到性能。
小結:
- 逆優化使得編譯器可以回到之前版本的編譯代碼
- 先前的優化不再有效時(例如,所涉及到的對象類型發生了更改),才會發生代碼逆優化。
- 代碼逆優化時,會對性能産生一些小而短暫的影響,不過新編譯的代碼會盡快地再次熱身。
- 分層編譯時,如果代碼之前由 client 編譯器編譯而現在 server 編譯器優化,就會發生逆優化。
分層編譯級别
程式使用分層編譯時,編譯日志會輸出所編譯的分層級别。
其中 client 編譯器有 3 種級别,server 編譯器有 2 種編譯級别,是以,編譯級别有以下幾種:
- 0:解釋代碼
- 1:簡單 C1 編譯代碼
- 2:受限的 C1 編譯代碼
- 3:完全 C1 編譯代碼
- 4:C2 編譯代碼
多數方法第一次編譯的級别是3 ,即完全 C1 編譯(不過所有方法都從級别0開始)。如果方法運作得足夠頻繁,它就會編譯成級别4(級别3的代碼就會被丢棄)。
如果 server 編譯器隊列滿了,就會從 server 隊列中取出方法, 以級别2進行編譯,在這個級别中,C1編譯器使用方法調用計數器和回邊計數器。這會讓方法編譯得更快,而方法也将在 C1 編譯器收集分析資訊之後被編譯為級别3,最終當 server 編譯器隊列不太忙的時候被編譯為級别4。
小結:
- 分層編譯可以在兩種編譯器和 5 種級别之間進行。
- 不建議人為更改級别。
小菜與你小結
- 不用擔心小方法,特别是getter和setter,因為它們很容易内聯。
- 需要編譯的代碼在編譯隊列中,隊列中的代碼越多,程式達到最佳性能的時間越久。
- 雖然代碼緩存的大小可以(也應該)調整,但它仍然是有限的資源。
- 代碼越簡單,優化越多,分析回報和逃逸分析可以使代碼更快,但複雜的循環結構和大方法限制了它的有效性。
Java面試題專欄
【71期】面試官:對并發熟悉嗎?談談你對Java中常用的幾種線程池的了解
【72期】面試官:對并發熟悉嗎?說一下synchronized與Lock的差別與使用
【73期】面試官:Spring 和 Spring Boot 的差別是什麼?
【74期】面試官:對多線程熟悉嗎,來談談線程池的好處?
【75期】面試官:說說Redis的過期鍵删除政策吧!(高頻)
【76期】面試官問:List如何一邊周遊,一邊删除?
【77期】這一道面試題就考驗了你對Java的了解程度
【78期】别找了,Java集合面試問題這裡幫你總結好了!
【79期】别找了,回答Spring中Bean的生命周期,這裡幫你總結好了!
【80期】說出Java建立線程的三種方式及對比
歡迎長按下圖關注公衆号後端技術精選