看看畢昇?JDK?團隊是如何解決?JVM?中?CMS?的?Crash
時間:2021-09-03 10:10:57
手機看文章
掃描二維碼
隨時隨地手機看文章
[導讀]編者按:筆者遇到一個非常典型JVM架構(gòu)相關(guān)問題,在x86正常運行的應用,在aarch64環(huán)境上低概率偶現(xiàn)JVM崩潰。這是一個典型的JVM內(nèi)部bug引發(fā)的問題。通過分析最終定位到CMS代碼存在bug,導致JVM在弱內(nèi)存模型的平臺上Crash。在分析過程中,涉及到CMS垃圾回收原理、...
編者按:筆者遇到一個非常典型 JVM 架構(gòu)相關(guān)問題,在 x86 正常運行的應用,在 aarch64 環(huán)境上低概率偶現(xiàn) JVM 崩潰。這是一個典型的 JVM 內(nèi)部 bug 引發(fā)的問題。通過分析最終定位到 CMS 代碼存在 bug,導致 JVM 在弱內(nèi)存模型的平臺上 Crash。在分析過程中,涉及到 CMS 垃圾回收原理、內(nèi)存屏障、對象頭、以及 ParNew
并行回收算法中多個線程競爭處理的相關(guān)技術(shù)。筆者發(fā)現(xiàn)并修復了該問題,并推送到上游社區(qū)中。畢昇 JDK 發(fā)布的所有版本均解決了該問題,其他 JDK 在 jdk8u292、jdk11.0.9、jdk13 以后的版本修復該問題。
bug 描述
目標進程在 aarch64 平臺上運行,使用的 GC 算法為CMS(-XX: UseConcMarkSweepGC)
,會概率性地發(fā)生 JVM crash,且問題發(fā)生的概率極低。我們在 aarch64 平臺上使用 fuzz 測試,運行目標進程 50w 次只出現(xiàn)過一次 crash(連續(xù)運行了 3 天)。JBS issue:https://bugs.openjdk.java.net/browse/JDK-8248851約束
- 我們對比了 x86 和 aarch64 架構(gòu),發(fā)現(xiàn)問題僅在 aarch64 環(huán)境下會出現(xiàn)。
- 文中引用的代碼段取自 openjdk-8u262:http://hg.openjdk.java.net/jdk8u/jdk8u-dev/。
- 讀者需要對 JVM 有基本的認知,如垃圾回收,對象布局,GC 線程等,且有一定的 C 基礎。
背景知識
GC
GC(Garbage Collection)是 JVM 中必不可少的部分,用于回收不再會被使用到的對象,同時釋放對象占用的內(nèi)存空間。垃圾回收對于釋放的剩余空間有兩種處理方式:- 一種是存活對象不移動,垃圾對象釋放的空間用空閑鏈表(free_list)來管理,通常叫做標記-清除(Mark-Sweep)。創(chuàng)建新對象時根據(jù)對象大小從空閑鏈表中選取合適的內(nèi)存塊存放新對象,但這種方式有兩個問題,一個是空間局部性不太好,還有一個是容易產(chǎn)生內(nèi)存碎片化的問題。
- 另一種對剩余空間的處理方式是 Copy GC,通過移動存活對象的方式,重新得到一個連續(xù)的空閑空間,創(chuàng)建新對象時總在這個連續(xù)的內(nèi)存空間分配,直接使用碰撞指針方式分配(Bump-Pointer)。這里又分兩種情況:
- 將存活對象復制到另一塊內(nèi)存(to-space,也叫 survival space),原內(nèi)存塊全部回收,這種方式叫撤離(Evacuation)。
- 將存活對象推向內(nèi)存塊的一側(cè),另一側(cè)全部回收,這種方式也被稱為標記-整理(Mark-Compact)。
- 年輕代,一般采用 Evacuation 方式的回收算法,沒有內(nèi)存碎片問題,但會造成部分空間浪費。
- 老年代,采用 Mark-Sweep 或者 Mark-Compact 算法,節(jié)省空間,但效率低。
CMS
CMS(Concurrent Mark Sweep)是一個以低時延為目標設計的 GC 算法,特點是 GC 的部分步驟可以和 mutator 線程(可理解為 Java 線程)同時進行,減少 STW(Stop-The-World)時間。年輕代使用 ParNewGC,是一種 Evacuation。老年代則采用 ConcMarkSweepGC,如同它的名字一樣,采用 Mark-Sweep(默認行為)和 Mark-Compact(定期整理碎片)方式回收,它的具體行為可以通過參數(shù)控制,這里就不展開了,不是本文的重點研究對象。CMS 是 openjdk 中實現(xiàn)較為復雜的 GC 算法,條件分支很多,閱讀起來也比較困難。在高版本 JDK 中已經(jīng)被更優(yōu)秀和高效的 G1 和 ZGC 替代(CMS 在 JDK 13 之后版本中被移除)。本文討論的重點主要是年輕代的回收,也就是 ParNewGC 。對象布局
在 Java 的世界中,萬物皆對象。對象存儲在內(nèi)存中的方式,稱為對象布局。在 JVM 中對象布局如下圖所示:對象由對象頭加字段組成,我們這里主要關(guān)注對象頭。對象頭包括markOop和_matadata。前者存放對象的標志信息,后者存放 Klass 指針。所謂 Klass,可以簡單理解為這個對象屬于哪個 Java 類,例如:String str = new String(); 對象 str 的 Klass 指針對應的 Java 類就是 Ljava/lang/String。- markOop 的信息很關(guān)鍵,它的定義如下[1]:
1.??//??32?bits:
2.??//??--------
3.??//??hash:25?------------>|?age:4??biased_lock:1?lock:2?(normal?object)
4.??//??JavaThread*:23?epoch:2?age:4??biased_lock:1?lock:2?(biased?object)
5.??//??size:32?------------------------------------------>|?(CMS?free?block)
6.??//??PromotedObject*:29?---------->|?promo_bits:3?----->|?(CMS?promoted?object)
7.??//
8.??//??64?bits:
9.??//??--------
10.??//??unused:25?hash:31?-->|?unused:1??age:4??biased_lock:1?lock:2?(normal?object)
11.??//??JavaThread*:54?epoch:2?unused:1??age:4??biased_lock:1?lock:2?(biased?object)
12.??//??PromotedObject*:61?--------------------->|?promo_bits:3?----->|?(CMS?promoted?object)
13.??//??size:64?----------------------------------------------------->|?(CMS?free?block)
14.??//
15.??//??unused:25?hash:31?-->|?cms_free:1?age:4??biased_lock:1?lock:2?(COOPs?