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