小師妹學JVM之:逃逸分析和TLAB
阿新 • • 發佈:2020-07-01
[toc]
# 簡介
逃逸分析我們在JDK14中JVM的效能優化一文中已經講過了,逃逸分析的結果就是JVM會在棧上分配物件,從而提升效率。如果我們在多執行緒的環境中,如何提升記憶體的分配效率呢?快來跟小師妹一起學習TLAB技術吧。
# 逃逸分析和棧上分配
小師妹:F師兄,從前大家都說物件是在堆中分配的,然後我就信了。上次你居然說可以在棧上分配物件,這個實在是顛覆了我一貫的認知啊。
柏拉圖說過:思想永遠是宇宙的統治者。只要思想不滑坡,辦法總比困難多。別人告訴你的都是一些最基本,最最通用的情況。而師兄我告訴你的則是在優化中的特列情況。
小師妹:F師兄,看起來JVM在提升執行速度方面真的做了不少優化呀。
是呀,Java從最開始被詬病速度慢,到現在執行速度直追C語言。這些執行時優化是必不可少的。還記得我們之前講的逃逸分析是怎麼回事嗎?
小師妹:F師兄,這個我知道,如果一個物件的分配是在方法內部,並且沒有多執行緒訪問的情況下,那麼這個物件其實可以看做是一個本地物件,這樣的物件不管建立在哪裡都只對本執行緒中的本方法可見,因此可以直接分配在棧空間中。
對的,棧上分配的物件因為不用考慮同步,所以執行速度肯定會更加快速,這也是為什麼JVM會引入棧上分配的原因。
再舉一個形象直觀的例子。工廠要組裝一輛汽車,在buildCar的過程中,需要先建立一個Car物件,然後給它按上輪子。
~~~java
public static void main(String[] args) {
buildCar();
}
public static void buildCar() {
Wheel whell = new Wheel(); //分配輪子
Car car = new Car(); //分配車子
car.setWheel(whell);
}
}
class Wheel {}
class Car {
private Wheel whell;
public void setWheel(Wheel whell) {
this.whell = whell;
}
}
~~~
考慮一下上面的情況,如果假設該車間是一個機器人組裝一臺車,那麼上面方法中建立的Car和Wheel物件,其實只會被這一個機器人訪問,其他的機器人根本就不會用到這個車的物件。那麼這個物件本質上是對其他機器人隱形的。所以我們可以不在公共空間分配這個物件,而是在私人的棧空間中分配。
逃逸分析還有一個作用就是lock coarsening。同樣的,單執行緒環境中,鎖也是不需要的,也可以優化掉。
# TLAB簡介
小師妹:F師兄,我覺得逃逸分析很好呀,棧上分配也不錯。既然又這麼厲害的兩項技術了,為什麼還要用到TLAB呢?
首先這是兩個不同的概念,TLAB的全稱是Thread-Local Allocation Buffers。Thread-Local大家都知道吧,就是執行緒的本地變數。而TLAB則是執行緒的本地分配空間。
逃逸分析和棧上分配只是爭對於單執行緒環境來說的,如果在多執行緒環境中,不可避免的會有多個執行緒同時在堆空間中分配物件的情況。
這種情況下如何處理才能提升效能呢?
小師妹:哇,多個執行緒競爭共享資源,這不是一個典型的鎖和同步的問題嗎?
鎖和同步是為了保證整個資源一次只能被一個執行緒訪問,我們現在的情況是要在資源中為執行緒劃分一定的區域。這種操作並不需要完全的同步,因為heap空間夠大,我們可以在這個空間中劃分出一塊一塊的小區域,為每個執行緒都分一塊。這樣不就解決了同步的問題了嗎?這也可以稱作空間換時間。
# TLAB詳解
小師妹,還記得heap分代技術中的一箇中心兩個基本點嗎?哦,1個Eden Space和2個Suvivor Space嗎?
![](https://img-blog.csdnimg.cn/20200602060126712.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70)
Young Gen被劃分為1個Eden Space和2個Suvivor Space。當物件剛剛被建立的時候,是放在Eden space。垃圾回收的時候,會掃描Eden Space和一個Suvivor Space。如果在垃圾回收的時候發現Eden Space中的物件仍然有效,則會將其複製到另外一個Suvivor Space。
就這樣不斷的掃描,最後經過多次掃描發現任然有效的物件會被放入Old Gen表示其生命週期比較長,可以減少垃圾回收時間。
因為TLAB關注的是新分配的物件,所以TLAB是被分配在Eden區間的,從圖上可以看到TLAB是一個一個的連續空間。
然後將這些連續的空間分配個各個執行緒使用。
因為每一個執行緒都有自己的獨立空間,所以這裡不涉及到同步的概念。預設情況下TLAB是開啟的,你可以通過:
~~~java
-XX:-UseTLAB
~~~
來關閉它。
## 設定TLAB空間的大小
小師妹,F師兄,這個TLAB的大小是系統預設的嗎?我們可以手動控制它的大小嗎?
要解決這個問題,我們還得去看JVM的C++實現,也就是threadLocalAllocBuffer.cpp:
![](https://img-blog.csdnimg.cn/20200602060906545.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70)
上面的程式碼可以看到,如果設定了TLAB(預設是0),那麼TLAB的大小是定義的TLABSize除以HeapWordSize和max_size()中最小的那個。
> HeapWordSize是heap中一個字的大小,我猜它=8。別問我為什麼,其實我也是猜的,有人知道答案的話可以留言告訴我。
TLAB的大小可以通過:
~~~java
-XX:TLABSize
~~~
來設定。
如果沒有設定TLAB,那麼TLAB的大小就是分配執行緒的平均值。
TLAB的最小值可以通過:
~~~java
-XX:MinTLABSize
~~~
來設定。
預設情況下:
~~~java
-XX:ResizeTLAB
~~~
resize開關是預設開啟的,那麼JVM可以對TLAB空間大小進行調整。
## TLAB中大物件的分配
小師妹:F師兄,我想到了一個問題,既然TLAB是有大小的,如果一個執行緒中定義了一個非常大的物件,TLAB放不下了,該怎麼辦呢?
好問題,這種情況下又有兩種可能性,我們假設現在的TLAB的大小是100K:
第一種可能性:
目前TLAB被使用了20K,還剩80K的大小,這時候我們建立了一個90K大小的物件,現在90K大小的物件放不進去TLAB,這時候需要直接在heap空間去分配這個物件,這種操作實際上是一種退化操作,官方叫做 slow allocation。
第二中個可能性:
目前TLAB被使用了90K,還剩10K大小,這時候我們建立了一個15K大小的物件。
這個時候就要考慮一下是否仍然進行slow allocation操作。
因為TLAB差不多已經用完了,為了保證後面new出來的物件仍然可以有一個TLAB可用,這時候JVM可以嘗試將現在的TLAB Retire掉,然後分配一個新的TLAB空間,把15K的物件放進去。
JVM有個開關,叫做:
~~~java
-XX:TLABWasteTargetPercent=N
~~~
這個開關的預設值是1。表示如果新分配的物件大小如果超出了設定的這個百分百,那麼就會執行slow allocation。否則就會分配一個新的TLAB空間。
同時JVM還定義了一個開關:
~~~java
-XX:TLABWasteIncrement=N
~~~
為了防止過多的slow allocation,JVM定義了這個開關(預設值是4),比如說第一次slow allocation的極限值是1%,那麼下一次slow allocation的極限值就是%1+4%=5%。
## TLAB空間中的浪費
小師妹:F師兄,如果新分配的TLAB空間,那麼老的TLAB中沒有使用的空間該怎麼辦呢?
這個叫做TLAB Waste。因為不會再在老的TLAB空間中分配物件了,所以剩餘的空間就浪費了。
# 總結
本文介紹了逃逸分析和TLAB的使用。希望大家能夠喜歡。
> 本文作者:flydean程式那些事
>
> 本文連結:[http://www.flydean.com/jvm-escapse-tlab/ ](http://www.flydean.com/jvm-escapse-tlab/ )
>
> 本文來源:flydean的部落格
>
> 歡迎關注我的公眾號:程式那些事,更多精彩等著您!