浅谈HotSpot逃逸分析
JIT
即時編譯(Just-in-time Compilation,JIT)是一種通過在運行時將字節碼翻譯為機器碼,從而改善字節碼編譯語言性能的技術。在HotSpot實現中有多種選擇:C1、C2和C1+C2,分別對應client、server和分層編譯。
1、C1編譯速度快,優化方式比較保守;
2、C2編譯速度慢,優化方式比較激進;
3、C1+C2在開始階段采用C1編譯,當代碼運行到一定熱度之后采用G2重新編譯;
在1.8之前,分層編譯默認是關閉的,可以添加-server -XX:+TieredCompilation參數進行開啟。
逃逸分析
逃逸分析并不是直接的優化手段,而是一個代碼分析,通過動態分析對象的作用域,為其它優化手段如棧上分配、標量替換和同步消除等提供依據,發生逃逸行為的情況有兩種:方法逃逸和線程逃逸。
1、方法逃逸:當一個對象在方法中定義之后,作為參數傳遞到其它方法中;
2、線程逃逸:如類變量或實例變量,可能被其它線程訪問到;
如果不存在逃逸行為,則可以對該對象進行如下優化:同步消除、標量替換和棧上分配。
同步消除
線程同步本身比較耗,如果確定一個對象不會逃逸出線程,無法被其它線程訪問到,那該對象的讀寫就不會存在競爭,則可以消除對該對象的同步鎖,通過-XX:+EliminateLocks可以開啟同步消除。
標量替換
1、標量是指不可分割的量,如java中基本數據類型和reference類型,相對的一個數據可以繼續分解,稱為聚合量;
2、如果把一個對象拆散,將其成員變量恢復到基本類型來訪問就叫做標量替換;
3、如果逃逸分析發現一個對象不會被外部訪問,并且該對象可以被拆散,那么經過優化之后,并不直接生成該對象,而是在棧上創建若干個成員變量;
通過-XX:+EliminateAllocations可以開啟標量替換, -XX:+PrintEliminateAllocations查看標量替換情況。
棧上分配
故名思議就是在棧上分配對象,其實目前Hotspot并沒有實現真正意義上的棧上分配,實際上是標量替換。
?
private static int fn(int age) {User user = new User(age);int i = user.getAge();return i;}User對象的作用域局限在方法fn中,可以使用標量替換的優化手段在棧上分配對象的成員變量,這樣就不會生成User對象,大大減輕GC的壓力,下面通過例子看看逃逸分析的影響。
?
public class JVM {public static void main(String[] args) throws Exception {int sum = 0;int count = 1000000;//warm upfor (int i = 0; i < count ; i++) {sum += fn(i);}Thread.sleep(500);for (int i = 0; i < count ; i++) {sum += fn(i);}System.out.println(sum);System.in.read();}private static int fn(int age) {User user = new User(age);int i = user.getAge();return i;} }class User {private final int age;public User(int age) {this.age = age;}public int getAge() {return age;} }分層編譯和逃逸分析在1.8中是默認是開啟的,例子中fn方法被執行了200w次,按理說應該在Java堆生成200w個User對象。
1、通過java -cp . -Xmx3G -Xmn2G -server -XX:-DoEscapeAnalysis JVM運行代碼,-XX:-DoEscapeAnalysis關閉逃逸分析,通過jps查看java進程的PID,接著通過jmap -histo [pid]查看java堆上的對象分布情況,結果如下:
可以發現:關閉逃逸分析之后,User對象一個不少的都在堆上進行分配。
?
2、通過java -cp . -Xmx3G -Xmn2G -server JVM運行代碼,結果如下:
可以發現:開啟逃逸分析之后,只有41w左右的User對象在Java堆上分配,其余的對象已經通過標量替換優化了。
?
3、通過java -cp . -Xmx3G -Xmn2G -server -XX:-TieredCompilation運行代碼,關閉分層編譯,結果如下:
可以發現:關閉了分層編譯之后,在Java堆上分配的User對象降低到1w多個,分層編譯對逃逸分析還是有影響的。
?
編譯閾值
即時編譯JIT只在代碼段執行足夠次數才會進行優化,在執行過程中不斷收集各種數據,作為優化的決策,所以在優化完成之前,例子中的User對象還是在堆上進行分配。
那么一段代碼需要執行多少次才會觸發JIT優化呢?通常這個值由-XX:CompileThreshold參數進行設置:
1、使用client編譯器時,默認為1500;
2、使用server編譯器時,默認為10000;
意味著如果方法調用次數或循環次數達到這個閾值就會觸發標準編譯,更改CompileThreshold標志的值,將使編譯器提早(或延遲)編譯。
除了標準編譯,還有一個叫做OSR(On Stack Replacement)棧上替換的編譯,如上述例子中的main方法,只執行一次,遠遠達不到閾值,但是方法體中執行了多次循環,OSR編譯就是只編譯該循環代碼,然后將其替換,下次循環時就執行編譯好的代碼,不過觸發OSR編譯也需要一個閾值,可以通過以下公式得到。
?
-XX:CompileThreshold = 10000 -XX:OnStackReplacePercentage = 140 -XX:InterpreterProfilePercentage = 33 OSR trigger = (CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)) / 100 = 10700其中trigger即為OSR編譯的閾值。
那么如果把CompileThreshold設置適當小一點,是不是可以提早觸發編譯行為,減少在堆上生成User對象?我們可以進行通過不同參數驗證一下:
1、-XX:CompileThreshold = 5000,結果如下:
2、-XX:CompileThreshold = 2500,結果如下:
?
3、-XX:CompileThreshold = 2000,結果如下:
?
4、-XX:CompileThreshold = 1500,結果如下:
?
在我的機器中,當設置到1500時,在堆上生成的User對象反而升到4w個,目前還不清楚原因是啥...
JIT編譯在默認情況是異步進行的,當觸發某方法或某代碼塊的優化時,先將其放入編譯隊列,然后由編譯線程進行編譯,編譯之后的代碼放在CodeCache中,CodeCache的大小也是有限的,通過-XX:-BackgroundCompilation參數可以關閉異步編譯,我們可以通過執行java -cp . -Xmx3G -Xmn2G -server -XX:CompileThreshold=1 -XX:-TieredCompilation -XX:-BackgroundCompilation JVM命令看看同步編譯的效果:在java堆上只生成了2個對象。
當然了,這是為了好玩而進行的測試,生產環境不要隨意修改這些參數:
1、熱點代碼的編譯過程是有成本的,如果邏輯復雜,編程成本更高;
2、編譯后的代碼會被存放在有大小限制的CodeCache中,如果CompileThreshold設置的太低,JIT會將一大堆執行不那么頻繁的代碼進行編譯,并放入CodeCache,導致之后真正執行頻繁的代碼沒有足夠的空間存放;
鏈接:https://www.jianshu.com/p/20bd2e9b1f03
?
總結
以上是生活随笔為你收集整理的浅谈HotSpot逃逸分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++ 二进制文件写操作
- 下一篇: fzu-2260