JVM垃圾收集器基準報告 – Ionuț Baloșin

banq發表於2019-12-16

本文使用一組不同的模式描述了一系列Java虛擬機器(JVM)垃圾收集器(GC)微基準及其結果。對於當前問題,我包括了AdoptOpenJDK 64位伺服器VM版本13(內部版本13 + 33)中的所有垃圾收集器:

  • 序列GC
  • Parallel / ParallelOld GC (啟動Java 7u4 ParallelGC和ParallelOld GC基本上是同一收集器)
  • 併發標記掃描CMS GC (目前不建議使用,它將根據JEP 363在Java 14版本中刪除)
  • 垃圾優先G1 GC
  • Shenandoah GC
  • ZGC (目前處於實驗階段)
  • Epsilon GC  (目前處於實驗狀態)

我故意選擇了AdoptOpenJDK,因為並非所有的OpenJDK構建都包括Shenandoah GC。

當前所有GC基準測試都集中在以下指標上:

  1. (a)在相對較小和較大物件的不同分配率下,GC物件回收的效率;(b)具有或不具有恆定的堆預分配部分。
  2. 遍歷和/或更新堆資料結構時嘗試讀寫屏障的影響,並嘗試避免在基準方法中進行任何顯式分配,除非它是由基礎生態系統引起的。
  3. 內部GC本機結構的佔用空間。

配置

  • 所有基準都是用Java編寫的,並使用JMH v1.22
  • 基準測試原始碼不是公開的,但我詳細介紹了它們所依賴的優化模式。
  • 每個基準測試使用5x10s的熱身迭代,5x10s的測量迭代,3個JVM分支,並且是單執行緒的。
  • 所有針對高物件分配率的基準測試,都將初始堆大小設定為與最大堆大小相同的值,而且還會預先觸控頁面以避免調整大小和記憶體提交打commit(例如-Xms4g -Xmx4g -XX:+ AlwaysPreTouch)。
  • 所有測試均在具有以下配置的計算機上啟動:

    • CPU:Intel i7-8550U Kaby Lake R
    • 記憶體:32GB DDR4 2400 MHz
    • 作業系統:Ubuntu 19.04 / 5.0.0-37-generic
  • 為了消除動態頻率縮放的影響,我禁用了intel_pstate驅動程式,並將CPU調節器設定為performance。
  • 請記住,當前的基準測試可能會受到其他因素的影響,例如即時編譯器優化,底層庫(例如JMH),CPU快取和分支預測效果,記憶體分配器子系統等。
  • 所有基準測試結果(包括吞吐量,gc.alloc.rate.norm,gc.count,gc.time等)都合併在我的GitHub帳戶的專用HTML報告中。為了獲得更好的圖表質量,我建議您開啟HTML報告,因為當前帖子僅包含列印螢幕(通常用於吞吐量測量)。您還可以在GitHub的同一儲存庫下找到原始基準測試結果(即JSON測試結果)。

一點理論

在進一步介紹之前,我想簡要地介紹一些理論,以更好地瞭解即將到來的基準測試。

什麼是垃圾回收機制(GC)? 它是一種自動記憶體管理形式。垃圾收集器嘗試回收由程式不再使用的物件所佔用的垃圾或記憶體。值得一提的是,垃圾回收器除了回收(對於不再可訪問的物件)外,還進行物件的分配。

分代 GC意味著將資料劃分為多個分配區域,這些區域根據物件的使用期限(即,倖存的GC迭代次數)保持分開。雖然有些收集器是單代的,但其他收集器則使用兩個堆代:

(1)年輕代(劃分為Eden 代和兩個Survivor代)

(2)老生代。

單代GC:

  • Shenandoah  GC
  • ZGC

兩代GC:

  • 序列GC
  • Parallel/ParallelOld GC
  • CMS GC
  • G1 GC

讀/寫屏障(Read/write barriers)是一種在對物件進行讀/寫時執行一些額外的記憶體管理程式碼的機制。即使沒有真正的GC發生,這些障礙通常也會影響應用程式的效能(只是讀/寫)。讓我們考慮以下虛擬碼:

object.field = some_other_object // write
 
object = some_other_object.field // read

使用讀/寫障礙的概念,從概念上講,它可能類似於:

write_barrier(object).field = some_other_object
 
object = read_barrier(some_other_object).field

關於上述AdoptOpenJDK收集器,使用的讀/寫障礙如下:

  • 寫屏障

    • 一個寫屏障(用於跟蹤從老生代到年輕代的引用,例如,卡片表),用於:

      • 序列GC
      • Parallel/ParallelOld GC
      • CMS GC
    • 一個寫屏障(在程式執行時併發標記的情況下,例如,Snapshot-At-The-Beginning (SATB)用於:

      • Shenandoah GC
    • 兩個寫屏障:(a)首先,在併發標記(例如SATB)的情況下是PreWrite屏障,(b)其次,再是PostWrite屏障,不僅要跟蹤從老生代到年輕代的引用,還要跟蹤任何跨區域引用(例如,記憶集):

      • G1 GC
  • 讀屏障

    • Shenandoah  GC

      • 在OpenJDK版本<= 12的情況下通過引用訪問物件的欄位(即,每次訪問都引用Brooks指標)時
      • 如果OpenJDK版本> = 13,則從堆中載入引用(即,載入引用屏障(LRB))時
    • ZGC,當從堆中載入引用時
  • 沒有障礙

    • Epsilon GC完全不使用任何屏障,並且可以用作所有其他收集器的基準

超出範圍

  • 有關每個垃圾收集器如何工作的更多詳細資訊。網際網路上有很多這樣的材料(例如演示,書籍,部落格等),其呈現方式比我可能寫的要好。
  • 任何其他JVM實現和GC型別(至少目前是這樣)。
  • 除4GB以外的任何其他JVM Heap大小。
  • 除了報告的JHM時序外,還有任何關於為何基準X優於或劣於基準Y的詳細說明。但是,我可以將資源提供給可能對重現該場景並進行進一步分析感興趣的任何HotSpot工程師。
  • 真實應用程式上的任何端到端巨集基準測試。這可能是最有代表性的,但是,當前的重點是微基準測試。

BurstHeapMemoryAllocator基準

該基準測試建立了許多臨時物件,在ArrayList中保持對它們的強引用,直到它填充了一定比例的Heap佔用率,然後釋放了它們(即呼叫blackhole.consume()),因此它們都突然有資格使用垃圾收集器。

結論

  • ZGC和Shenandoah GC的效能明顯優於其他所有收集器。
  • G1 GC的吞吐量比ZGC和Shenandoah GC差,但是在_4_MB物件(即,根據G1術語為巨大物件)的情況下,其效能明顯優於CMS GC,ParallelOld GC和序列GC。

(banq注:適合突然而來的尖鋒訪問)

ConstantHeapMemoryOccupancyBenchmark

此基準最初(在設定過程中)分配了許多物件,作為堆的預分配部分,並對其保持強烈引用,直到它填滿一定百分比的堆佔用率(例如70%)。預先分配的物件由大量的複合類組成(例如,類C1->類C2->…->類C32)。這可能會影響GC根遍歷(例如,在“並行”標記階段),因為遍歷物件圖時指標間接定向(即參考處理)的程度不可忽略。

然後,在基準測試方法中,分配了臨時物件(大小為8 MB)並立即釋放,因此它們很快就可以使用垃圾收集器。由於這些物件被認為是大物件,因此它們通常遵循緩慢的路徑分配,直接駐留在“老生代”中(對於代收集者而言),從而增加了使用完整GC的可能性。

結論

  • CMS GC和G1 GC的效能明顯優於其他所有GC。
  • 也許令人驚訝的是,ZGC和Shenandoah GC的吞吐量最差。

(banq注:程式有快取機制,啟動時預先warm了記憶體,載入了一些熱點資料在記憶體中,或者使用類似Redis原理的常駐記憶體機制)

HeapMemoryBandwidthAllocatorBenchmark

此基準測試分配大小不同塊的分配率。與以前的基準測試(例如,ConstantHeapMemoryOccupancyBenchmark)相比,它只是分配臨時物件並立即釋放它們,而沒有保留任何預分配的物件。

結論

  • 對於較大的物件(例如_4_MB),G1 GC的響應時間最差(比所有其他Collector慢5倍),而ParallelOld GC似乎是最高效的。
  • 對於相對較小的物件(例如_4_KB),結果幾乎相同,但對Shenandoah GC的支援略好一些,但沒有相關的區別。

對讀寫屏障測試點選標題見原文

  • 每個GC都有不同的記憶體佔用空間來儲存其內部GC本機結構。儘管這可能會受到堆大小的影響(即,增加/減小堆大小也可能會增加/減少GC本機結構的佔用空間),但很顯然,所有GC都需要額外的本機記憶體來進行堆管理。
  • 除Epsilon GC外,最小的記憶體屬於序列GC,其次是CMS GC,ZGC,Shenandoah GC,ParallelOld GC和G1 GC。

最終結論

請不要過於虔誠地接受此報告,因為它涵蓋了所有可能的用例。此外,某些基準可能存在缺陷,而另一些基準可能需要付出更多的努力才能深入研究並試圖理解這些數字背後的真正原因(超出範圍)。即使這樣,我認為它仍可以為您提供更廣泛的瞭解,並證明沒有一個垃圾收集器適合所有情況。雙方各有利弊,各有千秋。

根據當前的基準設定和此特定設定很難提供一般性結論。不過,我將其總結為:

  • 對於G1 GC,“記憶集”的管理有很大的開銷。
  • 當大量已分配例項(佔堆大小的60%)有資格進行回收時,ZGC和Shenandoah GC似乎非常有效。
  • 當堆Heap不包含許多其他可以在GC迭代之間存活的強可訪問例項時,ParallelOld GC可以很好地回收短期分配的物件。
  • CMS GC和G1 GC似乎提供了更好的吞吐量,同時當大量Heap堆(大約70%)不斷被佔用時,回收臨時分配的大物件(例如8_MB),因此在GC迭代之間可以生存的例項非常強大。

即使有一些通用的GC特性,您也可以大致瞭解哪種特性更適合您的應用程式:

  • 序列GC佔用空間最小,並且可能是參考實現(即最簡單的GC演算法)。
  • ParallelOld GC嘗試針對高吞吐量進行優化。
  • G1 GC努力在吞吐量和暫停時間之間取得平衡。
  • ZGC努力爭取儘可能短的暫停時間(例如,最大10毫秒),並且旨在從較小的堆大小到較大的堆大小(即,從數百MB到很多TB)更好地擴充套件。
  • Shenandoah GC的目標是低暫停時間,不再與堆大小成正比。

(banq注:吞吐量與暫停是一對矛盾,ParallelOld GC注重高吞吐量,而Shenandoah GC是注重低暫停,其他是在這兩個極端之間平滑)

 

相關文章