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


簡介

逃逸分析我們在JDK14中JVM的性能優化一文中已經講過了,逃逸分析的結果就是JVM會在棧上分配對象,從而提升效率。如果我們在多線程的環境中,如何提升內存的分配效率呢?快來跟小師妹一起學習TLAB技術吧。

逃逸分析和棧上分配

小師妹:F師兄,從前大家都說對象是在堆中分配的,然后我就信了。上次你居然說可以在棧上分配對象,這個實在是顛覆了我一貫的認知啊。

柏拉圖說過:思想永遠是宇宙的統治者。只要思想不滑坡,辦法總比困難多。別人告訴你的都是一些最基本,最最通用的情況。而師兄我告訴你的則是在優化中的特列情況。

小師妹:F師兄,看起來JVM在提升運行速度方面真的做了不少優化呀。

是呀,Java從最開始被詬病速度慢,到現在執行速度直追C語言。這些運行時優化是必不可少的。還記得我們之前講的逃逸分析是怎么回事嗎?

小師妹:F師兄,這個我知道,如果一個對象的分配是在方法內部,並且沒有多線程訪問的情況下,那么這個對象其實可以看做是一個本地對象,這樣的對象不管創建在哪里都只對本線程中的本方法可見,因此可以直接分配在棧空間中。

對的,棧上分配的對象因為不用考慮同步,所以執行速度肯定會更加快速,這也是為什么JVM會引入棧上分配的原因。

再舉一個形象直觀的例子。工廠要組裝一輛汽車,在buildCar的過程中,需要先創建一個Car對象,然后給它按上輪子。

  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嗎?

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是開啟的,你可以通過:

-XX:-UseTLAB

來關閉它。

設置TLAB空間的大小

小師妹,F師兄,這個TLAB的大小是系統默認的嗎?我們可以手動控制它的大小嗎?

要解決這個問題,我們還得去看JVM的C++實現,也就是threadLocalAllocBuffer.cpp:

上面的代碼可以看到,如果設置了TLAB(默認是0),那么TLAB的大小是定義的TLABSize除以HeapWordSize和max_size()中最小的那個。

HeapWordSize是heap中一個字的大小,我猜它=8。別問我為什么,其實我也是猜的,有人知道答案的話可以留言告訴我。

TLAB的大小可以通過:

-XX:TLABSize

來設置。

如果沒有設置TLAB,那么TLAB的大小就是分配線程的平均值。

TLAB的最小值可以通過:

-XX:MinTLABSize

來設置。

默認情況下:

-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有個開關,叫做:

-XX:TLABWasteTargetPercent=N

這個開關的默認值是1。表示如果新分配的對象大小如果超出了設置的這個百分百,那么就會執行slow allocation。否則就會分配一個新的TLAB空間。

同時JVM還定義了一個開關:

-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/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM