天天看點

全網最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析8.TLAB 基本流程

今天,又是幹貨滿滿的一天。這是全網最硬核 JVM 系列的開篇,首先從 TLAB 開始。由于文章很長,每個人閱讀習慣不同,是以特此拆成單篇版和多篇版
  • 全網最硬核 JVM TLAB 分析(單篇版不包含額外加菜)
  • 全網最硬核 JVM TLAB 分析 1. 記憶體配置設定思想引入
  • 全網最硬核 JVM TLAB 分析 2. TLAB生命周期與帶來的問題思考
  • 全網最硬核 JVM TLAB 分析 3. JVM EMA期望算法與TLAB相關JVM啟動參數
  • 全網最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析
  • 全網最硬核 JVM TLAB 分析 5. TLAB 源代碼全解析
  • 全網最硬核 JVM TLAB 分析 6. TLAB 相關熱門Q&A彙總
  • 全網最硬核 JVM TLAB 分析(額外加菜) 7. TLAB 相關 JVM 日志解析
  • 全網最硬核 JVM TLAB 分析(額外加菜) 8. 通過 JFR 監控 TLAB

8.TLAB 基本流程

8.0. 如何設計每個線程的 TLAB 大小

之前我們提到了引入 TLAB 要面臨的問題以及解決方式,根據這些我們可以這麼設計 TLAB。

首先,TLAB 的初始大小,應該和每個 GC 内需要對象配置設定的線程個數相關。但是,要配置設定的線程個數并不一定是穩定的,可能這個時間段線程數多,下個階段線程數就不那麼多了,是以,需要用 EMA 的算法采集每個 GC 内需要對象配置設定的線程個數來計算這個個數期望。

接着,我們最理想的情況下,是每個 GC 内,所有用來配置設定對象的記憶體都處于對應線程的 TLAB 中。每個 GC 内用來配置設定對象的記憶體從 JVM 設計上來講,其實就是 Eden 區大小。在 最理想的情況下,最好隻有Eden 區滿了的時候才會 GC,不會有其他原因導緻的 GC,這樣是最高效的情況。Eden 區被用光,如果全都是 TLAB 内配置設定,也就是 Eden 區被所有線程的 TLAB 占滿了,這樣配置設定是最快的。

然後,每輪 GC 配置設定記憶體的線程個數以及大小是不一定的,如果一下子配置設定一大塊會造成浪費,如果太小則會頻繁從 Eden 申請 TLAB,降低效率。這個大小比較難以控制,但是我們可以限制每個線程究竟在一輪 GC 内,最多從 Eden 申請多少次 TLAB,這樣對于使用者來說更好控制。

最後,每個線程配置設定的記憶體大小,在每輪 GC 并不一定穩定,隻用初始大小來指導之後的 TLAB 大小,顯然不夠。我們換個思路,每個線程配置設定的記憶體和曆史有一定關系是以我們可以從曆史配置設定中推測,是以每個線程也需要采用 EMA 的算法采集這個線程每次 GC 配置設定的記憶體,用于指導下次期望的 TLAB 的大小。

綜上所述,我們可以得出這樣一個近似的 TLAB 計算公式:

每個線程 TLAB 初始大小 =

Eden區大小

/ (

線程單個 GC 輪次内最多從 Eden 申請多少次 TLAB

*

目前 GC 配置設定線程個數 EMA

)

GC 後,重新計算 TLAB 大小 =

Eden區大小

線程單個 GC 輪次内最多從 Eden 申請多少次 TLAB

目前 GC 配置設定線程個數 EMA

接下來,我們來詳細分析 TLAB 的整個生命周期的每個流程。

8.1. TLAB 初始化

線程初始化的時候,如果 JVM 啟用了 TLAB(預設是啟用的, 可以通過

-XX:-UseTLAB

關閉),則會初始化 TLAB,在發生對象配置設定時,會根據期望大小申請 TLAB 記憶體。同時,在 GC 掃描對象發生之後,線程第一次嘗試配置設定對象的時候,也會重新申請 TLAB 記憶體。我們先隻關心初始化,初始化的流程圖如 圖08 所示:

全網最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析8.TLAB 基本流程

初始化時候會計算 TLAB 初始期望大小。這涉及到了 TLAB 大小的限制:

  • TLAB 的最小大小:通過

    MinTLABSize

    指定
  • TLAB 的最大大小:不同的 GC 中不同,G1 GC 中為大對象(humongous object)大小,也就是 G1 region 大小的一半。因為開頭提到過,在 G1 GC 中,大對象不能在 TLAB 配置設定,而是老年代。ZGC 中為頁大小的 8 分之一,類似的在大部分情況下 Shenandoah GC 也是每個 Region 大小的 8 分之一。他們都是期望至少有 8 分之 7 的區域是不用退回的減少選擇 Cset 的時候的掃描複雜度。對于其他的 GC,則是 int 數組的最大大小,這個和之前提到的填充 dummy object 有關,後面會提到詳細流程。

之後的流程裡面,無論何時,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的範圍内,為了避免啰嗦,我們不會再強調這個限制~~~!!! 之後的流程裡面,無論何時,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的範圍内,為了避免啰嗦,我們不會再強調這個限制~~~!!! 之後的流程裡面,無論何時,TLAB 的大小都會在這個 TLAB 的最小大小 到 TLAB 的最大大小 的範圍内,為了避免啰嗦,我們不會再強調這個限制~~~!!! 重要的事情說三遍~

TLAB 期望大小(desired size) 在初始化的時候會計算 TLAB 期望大小,之後再 GC 等操作回收掉 TLAB 需要重計算這個期望大小。根據這個期望大小,TLAB 在申請空間的時候每次申請都會以這個期望大小作為基準的空間作為 TLAB 配置設定空間。

8.1.1. TLAB 初始期望大小計算

如 圖08 所示,如果指定了 TLABSize,就用這個大小作為初始期望大小。如果沒有指定,則按照如下的公式進行計算:

堆給TLAB的空間總大小

/(

目前有效配置設定線程個數期望

重填次數配置

  1. 堆給 TLAB 的空間總大小:堆上能有多少空間配置設定給 TLAB,不同的 GC 算法不一樣,但是大多數 GC 算法的實作都是 Eden 區大小,例如:
    1. 傳統的已經棄用的 Parallel Scanvage 中,就是 Eden 區大小。參考:parallelScavengeHeap.cpp
    2. 預設的G1 GC 中是 (YoungList 區域個數減去 Survivor 區域個數) * 區域大小,其實就是 Eden 區大小。參考:g1CollectedHeap.cpp
    3. ZGC 中是 Page 剩餘空間大小,Page 類似于 Eden 區,是大部分對象配置設定的區域。參考:zHeap.cpp
    4. Shenandoah GC 中是 FreeSet 的大小,也是類似于 Eden 的概念。參考:shenandoahHeap.cpp
  2. 目前有效配置設定線程個數期望:這是一個全局 EMA,EMA 是什麼之前已經說明了,是一種計算期望的方式。有效配置設定線程個數 EMA 的最小權重是 TLABAllocationWeight。有效配置設定線程個數 EMA 在有線程進行第一次有效對象配置設定的時候進行采集,在 TLAB 初始化的時候讀取這個值計算 TLAB 期望大小。
  3. TLAB 重填次數配置(refills time):根據 TLABWasteTargetPercent 計算的次數,公式為。TLABWasteTargetPercent 的意義其實是限制最大浪費空間限制,為何重填次數與之相關後面會詳細分析。

8.1.2. TLAB 初始配置設定比例計算

如 圖08 所示,接下來會計算TLAB 初始配置設定比例。

線程私有配置設定比例 EMA:與有效配置設定線程個數 EMA對應,有效配置設定線程個數 EMA是對于全局來說,每個線程應該占用多大的 TLAB 的描述,而配置設定比例 EMA 相當于對于目前線程應該占用的總 TLAB 空間的大小的一種動态控制。

初始化的時候,配置設定比例其實就是等于

1/目前有效配置設定線程個數

。圖08 的公式,代入之前的計算 TLAB 期望大小的公式,消參簡化之後就是

1/目前有效配置設定線程個數

。這個值作為初始值,采集如線程私有的配置設定比例 EMA。

8.1.3. 清零線程私有統計資料

這些采集資料會用于之後的目前線程的配置設定比例的計算與采集,進而影響之後的目前線程 TLAB 期望大小。

8.2. TLAB 配置設定

TLAB 配置設定流程如 圖09 所示。

全網最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析8.TLAB 基本流程

8.2.1. 從線程目前 TLAB 配置設定

如果啟用了 TLAB(預設是啟用的, 可以通過

-XX:-UseTLAB

關閉),則首先從線程目前 TLAB 配置設定記憶體,如果配置設定成功則傳回,否則根據目前 TLAB 剩餘空間與目前最大浪費空間限制大小進行不同的配置設定政策。在下一個流程,就會提到這個限制究竟是什麼。

8.2.2. 重新申請 TLAB 配置設定

如果目前 TLAB 剩餘空間大于目前最大浪費空間限制(根據 圖08 的流程,我們知道這個初始值為 期望大小/TLABRefillWasteFraction),直接在堆上配置設定。否則,重新申請一個 TLAB 配置設定。

為什麼需要最大浪費空間呢?

當重新配置設定一個 TLAB 的時候,原有的 TLAB 可能還有空間剩餘。原有的 TLAB 被退回堆之前,需要填充好 dummy object。由于 TLAB 僅線程内知道哪些被配置設定了,在 GC 掃描發生時傳回 Eden 區,如果不填充的話,外部并不知道哪一部分被使用哪一部分沒有,需要做額外的檢查,如果填充已經确認會被回收的對象,也就是 dummy object, GC 會直接标記之後跳過這塊記憶體,增加掃描效率。反正這塊記憶體已經屬于 TLAB,其他線程在下次掃描結束前是無法使用的。這個 dummy object 就是 int 數組。為了一定能有填充 dummy object 的空間,一般 TLAB 大小都會預留一個 dummy object 的 header 的空間,也是一個

int[]

的 header,是以 TLAB 的大小不能超過int 數組的最大大小,否則無法用 dummy object 填滿未使用的空間。

但是,填充 dummy 也造成了空間的浪費,這種浪費不能太多,是以通過最大浪費空間限制來限制這種浪費。

新的 TLAB 大小,取如下兩個值中較小的那個:

  • 目前堆剩餘給 TLAB 可配置設定的空間,大部分 GC 的實作其實就是對應的 Eden 區剩餘大小:
    • 傳統的已經棄用的 Parallel Scanvage 中,就是 Eden 區剩餘大小。參考:parallelScavengeHeap.cpp
    • 預設的G1 GC 中是目前 Region 中剩餘大小,其實就是将 Eden 分區了。參考:g1CollectedHeap.cpp
    • Shenandoah GC 中是 FreeSet 的剩餘大小,也是類似于 Eden 的概念。參考:shenandoahHeap.cpp
  • TLAB 期望大小 + 目前需要配置設定的空間大小

當配置設定出來 TLAB 之後,根據 ZeroTLAB 配置,決定是否将每個位元組賦 0。在建立對象的時候,本來也要對每個字段賦初始值,大部分字段初始值都是 0,并且,在 TLAB 返還到堆時,剩餘空間填充的也是 int[] 數組,裡面都是 0。是以其實可以提前填充好。并且,TLAB 剛配置設定出來的時候,賦 0 也能利用好 Allocation prefetch 的機制适應 CPU 緩存行(Allocation prefetch 的機制會在另一個系列說明),是以可以通過打開 ZeroTLAB 來在配置設定 TLAB 空間之後立刻賦 0。

8.2.3. 直接從堆上配置設定

直接從堆上配置設定是最慢的配置設定方式。一種情況就是,如果目前 TLAB 剩餘空間大于目前最大浪費空間限制,直接在堆上配置設定。并且,還會增加目前最大浪費空間限制,每次有這樣的配置設定就會增加 TLABWasteIncrement 的大小,這樣在一定次數的直接堆上配置設定之後,目前最大浪費空間限制一直增大會導緻目前 TLAB 剩餘空間小于目前最大浪費空間限制,進而申請新的 TLAB 進行配置設定。

8.3. GC 時 TLAB 回收與重計算期望大小

相關流程如 圖10 所示,在 GC 前與 GC 後,都會對 TLAB 做一些操作。

全網最硬核 JVM TLAB 分析 4. TLAB 基本流程全分析8.TLAB 基本流程

8.3.1. GC 前的操作

在 GC 前,如果啟用了 TLAB(預設是啟用的, 可以通過

-XX:-UseTLAB

關閉),則需要将所有線程的 TLAB 填充 dummy Object 退還給堆,并計算并采樣一些東西用于以後的 TLAB 大小計算。

首先為了保證本次計算具有參考意義,需要先判斷是否堆上 TLAB 空間被用了一半以上,假設不足,那麼認為本輪 GC 的資料沒有參考意義。如果被用了一半以上,那麼計算新的配置設定比例,新的配置設定比例 = 線程本輪 GC 配置設定空間的大小 / 堆上所有線程 TLAB 使用的空間,這麼計算主要因為配置設定比例描述的是目前線程占用堆上所有給 TLAB 的空間的比例,每個線程不一樣,通過這個比例動态控制不同業務線程的 TLAB 大小。

8.3.2. GC 後的操作