Java 中的 TLAB 或線程局部分配緩衝區是什麼?
一、簡介
在本教程中,我們將了解線程本地分配緩衝區 (TLAB)。我們將了解它們是什麼、JVM 如何使用它們以及我們如何管理它們。
2. Java中的內存分配
Java 中的某些命令會分配內存。最明顯的是new
關鍵字,但還有其他關鍵字 - 例如,使用反射。
每當我們這樣做時,JVM 都必須為堆上的新對象留出一些內存。特別是,JVM 內存分配在 Eden 或 Young 空間中以這種方式進行所有分配。
在單線程應用程序中,這很容易。由於任何時候只能發生一個內存分配請求,因此線程可以簡單地獲取下一個合適大小的塊,我們就完成了:
然而,在多線程應用程序中,我們不能這麼簡單地做事情。如果我們這樣做,則存在兩個線程將在完全相同的時刻請求內存並且都將被給予完全相同的塊的風險:
為了避免這種情況,我們同步內存分配,以便兩個線程不能同時請求同一內存塊。然而,同步所有內存分配將使它們基本上成為單線程,這可能成為我們應用程序的巨大瓶頸。
3. 線程局部分配緩衝區
JVM 使用線程本地分配緩衝區 (Thread-Local Allocation Buffers, TLAB) 解決了這個問題。這些是為給定線程保留的堆內存區域,僅由該線程用於分配內存:
通過這種方式工作,不需要同步,因為只有一個線程可以從此緩衝區中拉取。緩衝區本身以同步方式分配,但這是一個不太頻繁的操作。
由於為對象分配內存是一種相對常見的情況,因此這可以帶來巨大的性能改進。但具體是多少?我們可以通過一個簡單的測試很容易地確定這一點:
@Test
public void testAllocations() {
long start = System.currentTimeMillis();
List<Object> objects = new ArrayList<>();
for (int i = 0; i < 1_000_000; ++i) {
objects.add(new Object());
}
Assertions.assertEquals(1_000_000, objects.size());
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
這是一個相對簡單的測試,但它可以完成工作。我們將為 1,000,000 個新Object
實例分配內存並記錄需要多長時間。然後我們可以運行它多次,無論有沒有 TLAB,並查看平均時間是多少(我們將在第 5 節中看到如何關閉 TLAB。):
我們可以清楚地看到差異。使用 TLAB 的平均時間為 33 ms,不使用 TLAB 的平均時間高達 110 ms。僅通過更改這一設置即可提高 230%。
3.1. TLAB 空間不足
顯然,我們的 TLAB 空間是有限的。那麼,當我們用完時會發生什麼?
如果我們的應用程序嘗試為新對象分配空間,而 TLAB 沒有足夠的可用空間,則 JVM 有四種可能的選擇:
- 它可以為此線程分配新的 TLAB 空間量,從而有效地增加可用量。
- 它可以從 TLAB 空間外部為該對象分配內存。
- 它可以嘗試使用垃圾收集器釋放一些內存。
- 它可能無法分配內存,而是拋出錯誤。
選項 #4 是我們的災難性情況,因此我們希望盡可能避免它,但如果其他情況無法發生,則這是一個選項。
JVM 使用許多複雜的啟發式方法來確定使用哪些其他選項,並且這些啟發式方法可能會在不同 JVM 和不同版本之間發生變化。然而,影響這一決定的最重要的細節包括:
- 一段時間內可能的分配數量。如果我們可能要分配大量對象,那麼增加 TLAB 空間將是更有效的選擇。如果我們可能分配很少的對象,那麼增加 TLAB 空間實際上可能效率較低。
- 正在請求的內存量。請求的內存越多,在 TLAB 空間之外分配內存的成本就越高。
- 可用內存量。如果 JVM 有大量可用內存,那麼增加 TLAB 空間比內存使用率非常高時要容易得多。
- 內存爭用量。如果 JVM 有很多線程,每個線程都需要內存,那麼增加 TLAB 空間可能比線程很少的情況要昂貴得多。
3.2.塔板容量
使用 TLAB 似乎是提高性能的絕佳方法,但總是有成本的。防止多個線程分配同一內存區域所需的同步使得 TLAB 本身的分配成本相對較高。如果 JVM 內存使用率特別高,我們可能還需要首先等待有足夠的內存可供分配。因此,我們理想情況下希望盡可能少地這樣做。
但是,如果為線程分配的 TLAB 空間量大於其所需的內存量,則該內存將閒置在那裡,基本上被浪費了。更糟糕的是,浪費這個空間會使其他線程更難獲得 TLAB 空間的內存,並且會使整個應用程序總體速度變慢。
因此,對於到底要分配多少空間存在爭議。分配太多,我們就浪費了空間。但分配太少,我們將花費比分配 TLAB 空間所需的時間更多的時間。
值得慶幸的是,JVM 將為我們處理所有這些,儘管我們很快就會看到如何根據需要調整它。
4. 查看 TLAB 用法
既然我們知道了 TLAB 是什麼以及它對我們的應用程序的影響,那麼我們如何才能看到它的實際效果呢?
不幸的是, jconsole
工具不像標準內存池那樣提供任何可見性。
然而,JVM 本身可以輸出一些診斷信息。這使用了新的統一 GC 日誌記錄機制,因此我們必須使用-Xlog:gc+tlab=trace
標誌啟動 JVM才能查看此信息。然後,這將定期打印有關 JVM 當前 TLAB 使用情況的信息。例如,在 GC 運行期間,我們可能會看到類似以下內容:
[0.343s][trace][gc,tlab] GC(0) TLAB: gc thread: 0x000000014000a600 [id: 10499] desired_size: 450KB slow allocs: 4 refill waste: 7208B alloc: 0.99999 22528KB refills: 42 waste 1.4% gc: 161384B slow: 59152B
這告訴我們,對於這個特定的線程:
- 當前 TLAB 大小為 450 KB (
desired_size
)。 - 自上次 GV 以來,在 TLAB 之外已有四次分配(
slow allocs
)。
請注意,確切的日誌記錄因 JVM 和版本而異。
5. 調整 TLAB 設置
我們已經了解了打開和關閉 TLAB 可能產生的影響,但是我們還能用它做什麼呢?我們可以通過在啟動應用程序時提供 JVM 參數來調整許多設置。
首先,讓我們實際看看如何關閉它。這是通過傳遞 JVM 參數-XX-UseTLAB.
設置此項將阻止 JVM 使用 TLAB 並強制它在每個內存分配上使用同步。
我們還可以保持 TLAB 啟用,但通過設置 JVM 參數-XX:-ResizeTLAB
來阻止其調整大小。這樣做意味著如果給定線程的 TLAB 已滿,則所有未來的分配都將在 TLAB 之外並需要同步。
我們還可以配置 TLAB 的大小。我們可以為 JVM 參數-XX:TLABSize
提供一個要使用的值。這定義了 JVM 應為每個 TLAB 使用的建議初始大小,因此它是每個線程要分配的大小。如果將其設置為 0(默認值),那麼 JVM 將根據 JVM 的當前狀態動態確定每個線程分配多少資源。
在允許 JVM 動態確定大小的情況下,我們還可以指定-XX:MinTLABSize
來給出每個線程的 TLAB 大小的下限。我們還設置-XX:MaxTLABSize
作為每個線程 TLAB 可以增長到的上限。
所有這些設置都已經有了合理的默認值,通常最好只使用這些設置,但如果我們發現存在問題,我們確實有一定程度的控制權。
六、總結
在本文中,我們了解了什麼是線程本地分配緩衝區、如何使用它們以及如何管理它們。下次當您的應用程序遇到任何性能問題時,請考慮是否需要對此進行調查。