1. 程式人生 > >小師妹學JVM之:逃逸分析和TLAB

小師妹學JVM之:逃逸分析和TLAB

[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的部落格 > > 歡迎關注我的公眾號:程式那些事,更多精彩等著您!