【譯】JVM Anatomy Park #22: 安全點檢查

原文地址:JVM Anatomy Park #22: Safepoint Polls

問題

所有這些問題的答案是同一個猿规。

理論

像 JVM 這種托管運行時系統(tǒng)譬巫,有時需要停止 Java 線程帚桩,執(zhí)行一些運行時代碼供嚎。比如,執(zhí)行 stop-the-world GC糯而∑仆校可以等待所有線程最終調(diào)用 JVM,比如申請內(nèi)存(經(jīng)常執(zhí)行 TLAB 替換)歧蒋,或者類似的操作。但是這不一定會發(fā)生州既!如果線程正在執(zhí)行某種頻繁的循環(huán)邏輯而不做別的事情怎么辦谜洽?

在大部分機器上停止運行的線程實際上是很簡單的:向線程發(fā)送一個信號,強制處理器中斷吴叶,等阐虚。停止線程正在執(zhí)行的操作,將控制權(quán)轉(zhuǎn)交給別處蚌卤。然而实束,這還不足以讓 Java 線程在任意位置停止奥秆,特別是如果你需要精確的垃圾回收。在這種情況下咸灿,你需要知道寄存器和棧中的內(nèi)容构订,這些內(nèi)容可能是你需要處理的對象引用”苁福或者如果你想要取消偏向鎖悼瘾,你需要精確的知道線程的狀態(tài)和獲取的鎖∩笮兀或者如果你想要逆優(yōu)化方法亥宿,你需要在不丟失執(zhí)行狀態(tài)和臨時值的安全位置操作。

因此像 Hotspot 這種現(xiàn)代 JVMs 實現(xiàn)了協(xié)作機制:線程經(jīng)常詢問是否應(yīng)該將控制權(quán)交給 VM砂沛,在線程生命周期中某些已知的位置烫扼,線程的狀態(tài)是已知的。當(dāng)所有線程都在已知的位置停止的時候碍庵,VM 被認為是到達了安全點映企。檢查安全點請求的代碼片段因此被稱為安全點檢查(safepoint polls)

這種實現(xiàn)需要滿足以下有趣的權(quán)衡:安全點檢查很少觸發(fā)進程停止,所以在不觸發(fā)時應(yīng)該非常高效怎抛。我們可以通過實驗觀測么卑吭?

實踐

考慮這個簡單的 JMH 測試用例:

import org.openjdk.jmh.annotations.*;

import java.util.concurrent.TimeUnit;

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class EmptyBench {
    @Benchmark
    public void emptyMethod() {
        // This method is intentionally left blank.
    }
}

你可能認為這個測試用例測量的是空方法,但是實際上這測量的是執(zhí)行測試用例最小的基礎(chǔ)設(shè)施代碼:統(tǒng)計迭代马绝,等待迭代執(zhí)行時間豆赏。這段代碼執(zhí)行的非常快富稻,所以可以使用 -prof perfasm 剖析執(zhí)行過程掷邦。

這是原裝 OpenJDK 8u191 的執(zhí)行結(jié)果:

3.60%    ...a2: movzbl 0x94(%r8),%r10d       ; load "isDone" field
0.63%  │  ...aa: add    $0x1,%rbp             ; iterations++;
32.82% │  ...ae: test   %eax,0x1765654c(%rip) ; global safepoint poll
58.14% │  ...b4: test   %r10d,%r10d           ; if !isDone, do the cycle again
       ╰  ...b7: je     ...a2

這個空方法被內(nèi)聯(lián)了,所有的調(diào)用成本都消除了椭赋,僅僅剩下基礎(chǔ)設(shè)施邏輯抚岗。

看到 “global safepoint poll” 了么?當(dāng)需要檢查點的時候哪怔,JVM 將會持有“檢查頁(polling page)”[1]宣蔚,任何對該頁的讀操作將會觸發(fā) 段錯誤 (SEGV)。當(dāng)安全點檢查最終觸發(fā) SEGV 時认境,控制權(quán)將會傳遞給任一存在的 SEGV 處理器胚委,JVM 已經(jīng)準(zhǔn)備好了一個!可以看一下 JVM_handle_linux_signal 如何處理段錯誤叉信。

所有這些技巧的目的是使得安全點的成本盡可能低亩冬,因為很多位置都需要安全點,但是幾乎不會觸發(fā)硼身」杓保基于這個原因覆享,使用 test %eax, (addr) 指令:當(dāng)安全點檢查沒有觸發(fā)的時候,這條指令沒有作用[2]营袜。這條指令的編碼也很緊湊撒顿,在 x86_64 平臺僅僅占用6字節(jié)。對于給定 JVM 進程连茧,檢查的頁地址是固定的核蘸,所以 JIT 生成的代碼可以使用相對 RIP 尋址(RIP-relative addressing):從當(dāng)前指令指針給定頁地址的偏移量,而不需要耗費空間編碼8字節(jié)絕對地址啸驯。

通常來說只有一個檢查頁來處理所有線程客扎,所以生成的代碼不需要辨別當(dāng)前執(zhí)行的線程。但是如果 VM 想要停止單個線程罚斗,如何操作徙鱼?JEP-312: "Thread-Local Handshakes" 給出了這個問題的答案。為 VM 提供了對單個線程觸發(fā)握手(handshake)檢查的能力针姿,當(dāng)前是通過為每個線程分配單獨的檢查頁實現(xiàn)的袱吆,檢查指令讀取線程局部存儲(thread-local storage)中的頁地址。[3][4]

這是原裝 OpenJDK 11.0.1 的執(zhí)行結(jié)果:

0.31%    ...70: movzbl 0x94(%r9),%r10d   ; load "isDone" field
0.19%  │  ...78: mov    0x108(%r15),%r11  ; reading the thread-local poll page addr
25.62% │  ...7f: add    $0x1,%rbp         ; iterations++;
35.10% │  ...83: test   %eax,(%r11)       ; thread-local handshake poll
34.91% │  ...86: test   %r10d,%r10d       ; if !isDone, do the cycle again
       ╰  ...89: je     ...70

這純粹是運行時的問題距淫,所以可以通過 -XX:-ThreadLocalHandshakes 關(guān)閉這個特性绞绒,生成的代碼將會與 8u191 中的一樣。這解釋了 8 與 11 測試結(jié)果不同的原因(讓我們馬上用 -prof perfnorm 執(zhí)行測試用例):

Benchmark                              Mode  Cnt  Score   Error  Units

# 8u191
EmptyBench.test                        avgt   15   0.383 ±  0.007  ns/op
EmptyBench.test:CPI                    avgt    3   0.203 ±  0.014   #/op
EmptyBench.test:L1-dcache-load-misses  avgt    3  ≈ 10??            #/op
EmptyBench.test:L1-dcache-loads        avgt    3   2.009 ±  0.291   #/op
EmptyBench.test:cycles                 avgt    3   1.021 ±  0.193   #/op
EmptyBench.test:instructions           avgt    3   5.024 ±  0.229   #/op

# 11.0.1
EmptyBench.test                        avgt   15   0.590 ±  0.023  ns/op ; +0.2 ns
EmptyBench.test:CPI                    avgt    3   0.260 ±  0.173   #/op
EmptyBench.test:L1-dcache-loads        avgt    3   3.015 ±  0.120   #/op ; +1 load
EmptyBench.test:L1-dcache-load-misses  avgt    3  ≈ 10??            #/op
EmptyBench.test:cycles                 avgt    3   1.570 ±  0.248   #/op ; +0.5 cycles
EmptyBench.test:instructions           avgt    3   6.032 ±  0.197   #/op ; +1 instruction

# 11.0.1, -XX:-ThreadLocalHandshakes
EmptyBench.test                        avgt   15   0.385 ±  0.007  ns/op
EmptyBench.test:CPI                    avgt    3   0.205 ±  0.027   #/op
EmptyBench.test:L1-dcache-loads        avgt    3   2.012 ±  0.122   #/op
EmptyBench.test:L1-dcache-load-misses  avgt    3  ≈ 10??            #/op
EmptyBench.test:cycles                 avgt    3   1.030 ±  0.079   #/op
EmptyBench.test:instructions           avgt    3   5.031 ±  0.299   #/op

所以線程局部握手增加了一次額外的 L1 命中加載榕暇,這耗費了大約半個周期蓬衡。這也為我們評估安全點檢查的成本提供了一些基準(zhǔn):L1 命中加載,大概額外耗費半個周期彤枢。

觀察

安全點和握手檢查是托管運行時系統(tǒng)實現(xiàn)中的小細節(jié)狰晚。它們經(jīng)常出現(xiàn)在生成代碼的熱路徑中,有時候會影響性能缴啡,特別是在密集的循環(huán)中壁晒。然而,對于運行時系統(tǒng)實現(xiàn)諸如垃圾回收业栅、鎖優(yōu)化秒咐、逆優(yōu)化等重要的特性,這些操作是必要的碘裕。

有許多安全點相關(guān)的優(yōu)化反镇,我們將會單獨討論。


  1. 在 Linux/POSIX 中娘汞,調(diào)用 mprotect(PROT_NONE)足夠了。
  2. 差不多吧夕玩。在 x86 中你弦,這條指令改變標(biāo)志位惊豺,但是下一條指令將會重寫,我們只需要注意別在 test 和相關(guān)的 jCC 之間再做安全點檢查禽作。
  3. 線程局部存儲是每個線程可以訪問的一段本地數(shù)據(jù)尸昧。在許多平臺中,寄存器的壓力不是很高旷偿,生成的代碼總是將線程局部存儲放在寄存器中烹俗。在 x86_64 中,存儲位置通常是 %r15萍程。
  4. 從技術(shù)上講幢妄,停止一部分線程不是一個“安全點”。但是當(dāng)線程局部握手(thread-local handshakes)啟動的時候茫负,可以通過握手所有線程實現(xiàn)安全點蕉鸳。對于“安全點”和“握手”的場景都支持。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末忍法,一起剝皮案震驚了整個濱河市潮尝,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饿序,老刑警劉巖勉失,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異原探,居然都是意外死亡乱凿,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門踢匣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來告匠,“玉大人,你說我怎么就攤上這事离唬『笞ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵输莺,是天一觀的道長戚哎。 經(jīng)常有香客問我,道長嫂用,這世上最難降的妖魔是什么型凳? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮嘱函,結(jié)果婚禮上甘畅,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好疏唾,可當(dāng)我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布蓄氧。 她就那樣靜靜地躺著,像睡著了一般槐脏。 火紅的嫁衣襯著肌膚如雪喉童。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天顿天,我揣著相機與錄音堂氯,去河邊找鬼。 笑死牌废,一個胖子當(dāng)著我的面吹牛咽白,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播畔规,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼局扶,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叁扫?” 一聲冷哼從身側(cè)響起三妈,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎莫绣,沒想到半個月后畴蒲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡对室,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年模燥,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掩宜。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡蔫骂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牺汤,到底是詐尸還是另有隱情辽旋,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布檐迟,位于F島的核電站补胚,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏追迟。R本人自食惡果不足惜溶其,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望敦间。 院中可真熱鬧瓶逃,春花似錦束铭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至代芜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浓利,已是汗流浹背挤庇。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贷掖,地道東北人嫡秕。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像苹威,于是被迫代替她去往敵國和親昆咽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實習(xí)到現(xiàn)在牙甫,面了一些公司掷酗,掛了不少,但最終還是拿到小米窟哺、百度泻轰、阿里、京東且轨、新浪浮声、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,213評論 11 349
  • 一旋奢、多線程 說明下線程的狀態(tài) java中的線程一共有 5 種狀態(tài)泳挥。 NEW:這種情況指的是,通過 New 關(guān)鍵字創(chuàng)...
    Java旅行者閱讀 4,665評論 0 44
  • Java8張圖 11至朗、字符串不變性 12屉符、equals()方法、hashCode()方法的區(qū)別 13爽丹、...
    Miley_MOJIE閱讀 3,696評論 0 11
  • 在一個方法內(nèi)部定義的變量都存儲在棧中筑煮,當(dāng)這個函數(shù)運行結(jié)束后,其對應(yīng)的棧就會被回收粤蝎,此時真仲,在其方法體中定義的變量將不...
    Y了個J閱讀 4,413評論 1 14
  • 這篇文章是我之前翻閱了不少的書籍以及從網(wǎng)絡(luò)上收集的一些資料的整理,因此不免有一些不準(zhǔn)確的地方初澎,同時不同JDK版本的...
    高廣超閱讀 15,565評論 3 83