1
初識(shí)Safepoint-GC中的Safepoint
最早接觸JVM中的安全點(diǎn)概念是在讀《深入理解Java虛擬機(jī)》那本書(shū)垃圾回收器章節(jié)的內(nèi)容時(shí)。相信大部分人也一樣,都是通過(guò)這樣的方式第一次對(duì)安全點(diǎn)有了初步認(rèn)識(shí)。不妨,先復(fù)習(xí)一下《深入理解Java虛擬機(jī)》書(shū)中安全點(diǎn)那一章節(jié)的內(nèi)容。
書(shū)中是在講解垃圾收集器-垃圾收集算法的章節(jié)引入安全點(diǎn)的介紹,為了快速準(zhǔn)確地完成GC Roots枚舉,避免為每條指令都生成對(duì)應(yīng)的OopMap造成大量存儲(chǔ)空間的浪費(fèi),只在“特定的位置”生成對(duì)應(yīng)的OopMap,這些位置被稱(chēng)為安全點(diǎn)。然后,書(shū)中提到了安全點(diǎn)位置的選擇標(biāo)準(zhǔn)是:是否能讓程序長(zhǎng)時(shí)間執(zhí)行;所以會(huì)在方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等處才會(huì)產(chǎn)生安全點(diǎn)。
書(shū)中還提到了JVM如何在GC時(shí)讓用戶(hù)線程在最近的安全點(diǎn)處停頓下來(lái):搶先式中斷和主動(dòng)式中斷。搶先式中斷不需要線程的執(zhí)行代碼主動(dòng)去配合,在GC發(fā)生時(shí),系統(tǒng)首先把所有用戶(hù)線程全部中斷,如果發(fā)現(xiàn)有用戶(hù)線程中斷的地方不在安全點(diǎn)上,就恢復(fù)這條線程執(zhí)行,讓它一會(huì)再重新中斷,直到跑到安全點(diǎn)上。而主動(dòng)式中斷的思想是當(dāng)GC需要中斷線程時(shí),不直接對(duì)線程操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志位,各個(gè)線程執(zhí)行過(guò)程時(shí)不停地主動(dòng)去輪詢(xún)這個(gè)標(biāo)志,一旦發(fā)現(xiàn)中斷標(biāo)志為真就自己在最近的安全點(diǎn)上主動(dòng)中斷掛起。現(xiàn)在基本上所有虛擬機(jī)實(shí)現(xiàn)都采用主動(dòng)式中斷方式來(lái)暫停線程響應(yīng)GC事件。
總結(jié)一下初識(shí)安全點(diǎn)學(xué)到的知識(shí)點(diǎn):
JVM GC時(shí)需要讓用戶(hù)線程在安全點(diǎn)處停頓下來(lái)(Stop The World)
JVM會(huì)在方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等處放置安全點(diǎn)
JVM通過(guò)主動(dòng)中斷方式到達(dá)全局STW:設(shè)置一個(gè)標(biāo)志位,各個(gè)線程執(zhí)行過(guò)程時(shí)不停地主動(dòng)去輪詢(xún)這個(gè)標(biāo)志,一旦發(fā)現(xiàn)中斷標(biāo)志為真就自己在最近的安全點(diǎn)上主動(dòng)中斷掛起。
以上基本上就是《深入理解Java虛擬機(jī)》這本書(shū)對(duì)JVM安全點(diǎn)的所有介紹了,當(dāng)時(shí)覺(jué)得安全點(diǎn)還是很好理解,認(rèn)為安全點(diǎn)就是在垃圾回收時(shí)為了STW而設(shè)計(jì)的。
后來(lái)發(fā)現(xiàn),經(jīng)過(guò)一些線上問(wèn)題和網(wǎng)上看到有關(guān)安全點(diǎn)有趣的示例,發(fā)現(xiàn)安全點(diǎn)其實(shí)也不簡(jiǎn)單,不只有GC才會(huì)用到安全點(diǎn);簡(jiǎn)單的代碼如果寫(xiě)的不當(dāng),安全點(diǎn)也會(huì)帶來(lái)一些莫名其妙的問(wèn)題;其在JVM內(nèi)部的實(shí)現(xiàn)以及JIT對(duì)它的優(yōu)化,也經(jīng)常讓人摸不著頭腦。本文嘗試在初識(shí)安全點(diǎn)后已知知識(shí)點(diǎn)的基礎(chǔ)上,通過(guò)一段簡(jiǎn)單的示例代碼,多問(wèn)幾個(gè)為什么,來(lái)進(jìn)一步更全面的了解一下安全點(diǎn)。
2
通過(guò)一段示例代碼深入剖析Safepoint
2.1 示例代碼
這段示例代碼可直接復(fù)制到本地運(yùn)行,本文所有對(duì)示例代碼的運(yùn)行環(huán)境都是jdk 1.8。
public class SafePointTest { public static AtomicInteger counter = new AtomicInteger(0); public static void main(String[] args) throws Exception{ long startTime = System.currentTimeMillis(); Runnable runnable = () -> { System.out.println(interval(startTime) + "ms后," + Thread.currentThread().getName() + "子線程開(kāi)始運(yùn)行"); for(int i = 0; i < 100000000; i++) { counter.getAndAdd(1); } System.out.println(interval(startTime) + "ms后," + Thread.currentThread().getName() + "子線程結(jié)束運(yùn)行, counter=" + counter); }; Thread t1 = new Thread(runnable, "zz-t1"); Thread t2 = new Thread(runnable, "zz-t2"); t1.start(); t2.start(); System.out.println(interval(startTime) + "ms后,主線程開(kāi)始sleep."); Thread.sleep(1000L); System.out.println(interval(startTime) + "ms后,主線程結(jié)束sleep."); System.out.println(interval(startTime) + "ms后,主線程結(jié)束,counter:" + counter); } private static long interval(Long startTime) { return System.currentTimeMillis() - startTime; } }示例代碼中主線程啟動(dòng)兩個(gè)子線程,然后主線程睡眠1s,通過(guò)打印時(shí)間來(lái)觀察主線程和子線程的執(zhí)行情況。 按道理來(lái)說(shuō)這里主線程和兩個(gè)子線程獨(dú)立并發(fā),沒(méi)有任何顯性的依賴(lài),主線程的執(zhí)行是不會(huì)受子線程影響的:主線程睡眠結(jié)束后會(huì)直接結(jié)束。但是執(zhí)行結(jié)果卻和期望不一樣。 執(zhí)行結(jié)果如下方動(dòng)圖展示:
從執(zhí)行結(jié)果看,主線程在啟動(dòng)兩個(gè)線程后進(jìn)入睡眠狀態(tài),代碼中指定睡眠時(shí)間為1s,但是主線程卻在3s多之后才睡眠結(jié)束。是什么導(dǎo)致了主線程睡過(guò)頭了呢,從結(jié)果來(lái)看主線程睡覺(jué)結(jié)束時(shí)間和子線程結(jié)束時(shí)間是一致的。所以,我們有理由懷疑主線程沒(méi)有按時(shí)提前結(jié)束應(yīng)該是被兩個(gè)子線程阻塞了。
2.2 先給結(jié)論
由于VMThread的某些操作需要STW,主線程在sleep結(jié)束前進(jìn)入了JVM全局安全點(diǎn),然后主線程要等待其他線程全部進(jìn)入安全點(diǎn),所以主線程被長(zhǎng)時(shí)間沒(méi)有進(jìn)入安全點(diǎn)的其他線程給阻塞了。
2.3 驗(yàn)證結(jié)論
添加JVM打印安全點(diǎn)日志參數(shù)-XX:+PrintSafepointStatistics后再執(zhí)行上面的實(shí)例代碼,結(jié)果如下截圖:
可以從安全點(diǎn)日志中看到,JVM想要執(zhí)行no vm operation,這個(gè)操作需要線程進(jìn)入安全點(diǎn),整個(gè)期間有12個(gè)線程,正在運(yùn)行的線程有兩個(gè),需要等待這兩個(gè)線程進(jìn)入安全點(diǎn),等待耗時(shí)2251ms。
加上 -XX:+SafepointTimeout 和-XX:SafepointTimeoutDelay=2000 參數(shù)后執(zhí)行代碼可以進(jìn)一步看等待哪兩個(gè)線程進(jìn)入安全點(diǎn)。
果然和猜測(cè)的一樣,沒(méi)有到達(dá)安全點(diǎn)的兩個(gè)線程正是示例代碼中定義的zz-t1和zz-t2線程。
2.4 為什么
到這里這個(gè)示例的執(zhí)行結(jié)果的原因已經(jīng)有了結(jié)論并且得到了驗(yàn)證,基本上已經(jīng)知其然了。但是如果深入思考一下,初識(shí)安全點(diǎn)時(shí)學(xué)到的知識(shí)點(diǎn)還不能解釋?zhuān)詾榱酥渌匀唬@里提了幾個(gè)為什么。
(1)為什么會(huì)進(jìn)入安全點(diǎn)
換句話(huà)問(wèn),是什么觸發(fā)了進(jìn)入安全點(diǎn)?
由初識(shí)安全點(diǎn)得到的基礎(chǔ)知識(shí)知道進(jìn)入安全點(diǎn)需要兩個(gè)條件:
JVM操作設(shè)置了主動(dòng)中斷標(biāo)志
運(yùn)行的代碼中存在安全點(diǎn)
首先想到的是GC觸發(fā)JVM設(shè)置主動(dòng)中斷標(biāo)志,加上 -XX:-PrintGC再執(zhí)行示例代碼并沒(méi)有打印 GC 日志,可以排除掉GC。
既然不是GC,還是再回到安全點(diǎn)日志上尋找線索吧,發(fā)現(xiàn)有個(gè)vmop(虛擬機(jī)操作類(lèi)型):no vm operation,關(guān)于no vm operation,網(wǎng)上有大神通過(guò)解析JVM源碼得到了結(jié)論,這里不對(duì)JVM源碼展開(kāi)做詳細(xì)解讀,直接給結(jié)論:
在 JVM 正常運(yùn)行的時(shí)候,如果設(shè)置了進(jìn)入安全點(diǎn)的間隔,就會(huì)隔一段時(shí)間判斷是否有代碼緩存要清理,如果有,會(huì)進(jìn)入安全點(diǎn)。這個(gè)觸發(fā)條件不是 VM 操作,所以會(huì)將 _vmop_type 設(shè)置成-1,輸出日志的時(shí)候打印對(duì)應(yīng)的 「no vm operation」,也就是我們看到的安全點(diǎn)日志。
在 VM 操作為空的情況下,只要滿(mǎn)足以下 3 個(gè)條件,也是會(huì)進(jìn)入安全點(diǎn)的:
1、VMThread 處于正常運(yùn)行狀態(tài)
2、設(shè)置了進(jìn)入安全點(diǎn)的間隔時(shí)間
3、SafepointALot 是否為 true 或者是否需要清理
用 Java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint 命令查看 JVM 關(guān)于安全點(diǎn)的默認(rèn)參數(shù):
發(fā)現(xiàn) GuaranteedSafepointInterval 默認(rèn)設(shè)置成了 1 秒,每隔1s就會(huì)嘗試進(jìn)入安全點(diǎn)。
那么,修改GuaranteedSafepointInterval參數(shù)值,看看是否能阻止進(jìn)入安全點(diǎn)。
GuaranteedSafepointInterval參數(shù)是JVM診斷參數(shù),修改這個(gè)參數(shù)的值,需要配合-XX:+UnlockDiagnosticVMOptions一起使用。
另外不建議在線上對(duì)這個(gè)參數(shù)的值做修改。
關(guān)閉定時(shí)進(jìn)入安全點(diǎn)
通過(guò) -XX:GuaranteedSafepointInterval = 0 關(guān)閉定時(shí)進(jìn)入安全點(diǎn),看看代碼運(yùn)行結(jié)果是怎么樣的
由運(yùn)行結(jié)果可以看出,關(guān)閉定時(shí)進(jìn)入安全點(diǎn)后,主線程睡眠1s后正常結(jié)束,不受其他線程阻塞。從安全點(diǎn)日志看,之前等待進(jìn)入安全點(diǎn)的兩個(gè)線程也沒(méi)有了。
調(diào)大定時(shí)進(jìn)入安全點(diǎn)間隔時(shí)間
由打印的執(zhí)行結(jié)果可以看到子線程運(yùn)行時(shí)間是3s多,如果把進(jìn)入安全點(diǎn)間隔時(shí)間調(diào)整為5s,即在子線程結(jié)束之后再?lài)L試進(jìn)入安全點(diǎn)是不是也能避免等待子線程進(jìn)入安全點(diǎn)呢? 修改參數(shù)-XX:GuaranteedSafepointInterval = 5000 調(diào)整安全點(diǎn)間隔時(shí)間再次執(zhí)行結(jié)果:
從執(zhí)行結(jié)果可以看出,調(diào)大安全點(diǎn)間隔時(shí)間和關(guān)閉定時(shí)進(jìn)入安全點(diǎn)的效果是一樣的,也可以避免等待子線程進(jìn)入安全點(diǎn)的。
(2)主線程是在哪里進(jìn)入的安全點(diǎn)
從示例代碼在默認(rèn)JVM參數(shù)執(zhí)行結(jié)果看,主線程睡眠時(shí)間超過(guò)了3s,事實(shí)上主線程是在Thread.sleep()方法內(nèi)部進(jìn)入安全點(diǎn)。這里對(duì)JVM 安全點(diǎn)實(shí)現(xiàn)的源碼簡(jiǎn)單做一下分析:
Safepoint實(shí)現(xiàn)源代碼:Safepoint.cpp
讀源碼太費(fèi)勁,看注釋吧,所幸從注釋中也能找到答案。上面截圖的注釋說(shuō)在程序進(jìn)入 Safepoint 的時(shí)候,Java 線程可能正處于的五種不同的狀態(tài),針對(duì)不同的狀態(tài)的不同處理機(jī)制。假設(shè)現(xiàn)在有一個(gè)操作觸發(fā)了某個(gè) VM 線程所有線程需要進(jìn)入 SafePoint,如果其他線程現(xiàn)在:
運(yùn)行字節(jié)碼:運(yùn)行字節(jié)碼時(shí),解釋器會(huì)看線程是否被標(biāo)記為 poll armed,如果是,VM 線程調(diào)用 SafepointSynchronize::block(JavaThread *thread)進(jìn)行 block。
運(yùn)行 native 代碼:當(dāng)運(yùn)行 native 代碼時(shí),VM 線程略過(guò)這個(gè)線程,但是給這個(gè)線程設(shè)置 poll armed,讓它在執(zhí)行完 native 代碼之后,它會(huì)檢查是否 poll armed,如果還需要停在 SafePoint,則直接 block。
運(yùn)行 JIT 編譯好的代碼:由于運(yùn)行的是編譯好的機(jī)器碼,直接查看本地 local polling page 是否為臟,如果為臟則需要 block。這個(gè)特性是在 Java 10 引入的 JEP 312: Thread-Local Handshakes 之后,才是只用檢查本地 local polling page 是否為臟就可以了。
處于 BLOCK 狀態(tài):在需要所有線程需要進(jìn)入 SafePoint 的操作完成之前,不許離開(kāi) BLOCK 狀態(tài)
處于線程切換狀態(tài)或者處于 VM 運(yùn)行狀態(tài):會(huì)一直輪詢(xún)線程狀態(tài)直到線程處于阻塞狀態(tài)(線程肯定會(huì)變成上面說(shuō)的那四種狀態(tài),變成哪個(gè)都會(huì) block 住)。
再看一下Thread.sleep方法的聲明,就和上面Safepoint.cpp源碼注釋截圖紅框?qū)ι狭耍琓hread.sleep正是一個(gè)native方法。
Thread.sleep(0)在RocketMQ中的妙用
上面這段代碼是RocketMQ的一段代碼,16年最早版本的實(shí)現(xiàn)for循環(huán)內(nèi)每循環(huán)1000次會(huì)調(diào)用一次Thread.sleep(0),這貌似是一段無(wú)用的代碼,作者真實(shí)的目的是為了在這里放置一個(gè)安全點(diǎn),避免for循環(huán)運(yùn)行時(shí)間過(guò)長(zhǎng)導(dǎo)致系統(tǒng)長(zhǎng)時(shí)間SWT。從代碼的變更記錄看,22年9月份有人對(duì)這段代碼換了一種寫(xiě)法:把for循環(huán)變量類(lèi)型定義成long型,同時(shí)注釋掉了循環(huán)內(nèi)部Thread.sleep(0)代碼,為什么可以這樣寫(xiě)以及為什么要這樣寫(xiě)這里先按下不表。
(3)子線程為什么無(wú)法進(jìn)入安全點(diǎn)
現(xiàn)在已經(jīng)知道了主線程為什么進(jìn)入會(huì)進(jìn)入安全點(diǎn),以及主線程在哪里進(jìn)入的安全點(diǎn),按照已知知識(shí)點(diǎn)JVM會(huì)在循環(huán)跳轉(zhuǎn)處和方法調(diào)用處放置安全點(diǎn),為什么子線程沒(méi)有進(jìn)入安全點(diǎn)?
可數(shù)循環(huán)和不可數(shù)循環(huán)
JVM為了避免安全點(diǎn)過(guò)多帶來(lái)過(guò)重的負(fù)擔(dān),對(duì)循環(huán)有一項(xiàng)優(yōu)化措施,認(rèn)為循環(huán)次數(shù)較少的話(huà),執(zhí)行時(shí)間應(yīng)該不會(huì)太長(zhǎng),所以使用int類(lèi)型和范圍更小的數(shù)據(jù)類(lèi)型作為索引值的循環(huán)默認(rèn)是不會(huì)被放置安全點(diǎn)的。這種循環(huán)被稱(chēng)為可數(shù)循環(huán),相對(duì)應(yīng)的,使用long或者范圍更大的數(shù)據(jù)類(lèi)型作為索引值的循環(huán)就被稱(chēng)為不可數(shù)循環(huán),將被放置安全點(diǎn)。
在示例代碼中,子線程的循環(huán)索引值數(shù)據(jù)類(lèi)型是int,也就是可數(shù)循環(huán),所以JVM沒(méi)有在循環(huán)跳轉(zhuǎn)處放置安全點(diǎn)。
把循環(huán)索引值數(shù)據(jù)類(lèi)型改成long型,循環(huán)成為不可數(shù)循環(huán),就能夠成功在循環(huán)跳轉(zhuǎn)處放置安全點(diǎn),避免子線程長(zhǎng)時(shí)間無(wú)法進(jìn)入安全點(diǎn)阻塞主線程。
從上面的執(zhí)行結(jié)果可以看到,把循環(huán)索引值數(shù)據(jù)類(lèi)型改成long型,主線程在睡眠1s之后立即結(jié)束了睡眠,并沒(méi)有等待子線程的執(zhí)行。
到這里,也就知道為什么上面貼的RocketMQ大那段代碼,把循環(huán)索引值數(shù)據(jù)類(lèi)型改成long型可以替換循環(huán)內(nèi)部Thread.Sleep(0)達(dá)到放置安全點(diǎn)的目的了。
其實(shí),還可以通過(guò)-XX:+UseCountedLoopSafepoints參數(shù)關(guān)閉JVM 對(duì)可數(shù)循環(huán)放置安全點(diǎn)的優(yōu)化。下面的執(zhí)行結(jié)果可以看出,添加了-XX:+UseCountedLoopSafepoints參數(shù)后,也能讓運(yùn)行結(jié)果到達(dá)預(yù)期。
還有一個(gè)疑惑
仔細(xì)看實(shí)例代碼,發(fā)現(xiàn)子線程循環(huán)體內(nèi)調(diào)用了AtomicInteger類(lèi)的getAndAdd方法,再深入看jdk getAndAdd方法的實(shí)現(xiàn),發(fā)現(xiàn)底層是調(diào)用了sun.misc.Unsafe#getIntVolatile 這個(gè)方法和Thread.sleep方法一樣,也是一個(gè)native方法,為什么這里沒(méi)有進(jìn)入像Thread.sleep方法一樣進(jìn)入安全點(diǎn)?
是的,好可怕,確實(shí)被優(yōu)化了,被 JIT給優(yōu)化了。為了驗(yàn)證是被JIT優(yōu)化了,可以用
-Djava.compiler=NONE關(guān)閉JIT然后看一下運(yùn)行結(jié)果。
從運(yùn)行結(jié)果看,關(guān)閉了JIT優(yōu)化后,主線程確實(shí)在睡眠1s后立即結(jié)束了,不過(guò)子線程運(yùn)行的時(shí)間比JIT優(yōu)化開(kāi)啟時(shí)多了不少。所以,JIT還是能夠帶來(lái)一定的性能優(yōu)化的,有時(shí)也會(huì)帶來(lái)一些奇怪的現(xiàn)象。
3
更全面的安全點(diǎn)定義
區(qū)別于初識(shí)安全點(diǎn)的時(shí)候局限于GC中的安全點(diǎn)概念,這里給安全點(diǎn)一個(gè)比較全面的定義:
Safepoint 可以理解成是在代碼執(zhí)行過(guò)程中的一些特殊位置,當(dāng)線程執(zhí)行到這些位置的時(shí)候,線程可以暫停。在 SafePoint 保存了其他位置沒(méi)有的一些當(dāng)前線程的運(yùn)行信息,供其他線程讀取。這些信息包括:線程上下文的任何信息,例如對(duì)象或者非對(duì)象的內(nèi)部指針等等。我們一般這么理解 SafePoint,就是線程只有運(yùn)行到了 SafePoint 的位置,他的一切狀態(tài)信息,才是確定的,也只有這個(gè)時(shí)候,才知道這個(gè)線程用了哪些內(nèi)存,沒(méi)有用哪些;并且,只有線程處于 SafePoint 位置,這時(shí)候?qū)?JVM的堆棧信息進(jìn)行修改,例如回收某一部分不用的內(nèi)存,線程才會(huì)感知到,之后繼續(xù)運(yùn)行,每個(gè)線程都有一份自己的內(nèi)存使用快照,這時(shí)候其他線程對(duì)于內(nèi)存使用的修改,線程就不知道了,只有再進(jìn)行到 SafePoint 的時(shí)候,才會(huì)感知。
4
什么時(shí)候會(huì)進(jìn)入Safepoint
當(dāng)VM Thread需要做vm 操作時(shí)會(huì)讓線程進(jìn)入安全點(diǎn),vm操作類(lèi)型有很多,可以參考VM_OP_ENUM源碼 vmOperations.hpp。下面是幾種經(jīng)常發(fā)生的進(jìn)入Safepoint的情形:
(1)GC:由于需要每個(gè)線程的對(duì)象使用信息,以及回收一些對(duì)象,釋放某些堆內(nèi)存或者直接內(nèi)存,所以需要 進(jìn)入Safepoint來(lái) Stop the world;
(2)定時(shí)進(jìn)入 SafePoint:每經(jīng)過(guò)-XX:GuaranteedSafepointInterval 配置的時(shí)間,都會(huì)讓所有線程進(jìn)入 Safepoint,一旦所有線程都進(jìn)入,立刻從 Safepoint 恢復(fù)。這個(gè)定時(shí)主要是為了一些沒(méi)必要立刻 Stop the world 的任務(wù)執(zhí)行,可以設(shè)置-XX:GuaranteedSafepointInterval=0關(guān)閉這個(gè)定時(shí)。
(3)由于 jstack,jmap 和 jstat 等命令,會(huì)導(dǎo)致 Stop the world:這種命令都需要采集堆棧信息,所以需要所有線程進(jìn)入 Safepoint 并暫停。
(4)偏向鎖取消:鎖大部分情況是沒(méi)有競(jìng)爭(zhēng)的(某個(gè)同步塊大多數(shù)情況都不會(huì)出現(xiàn)多線程同時(shí)競(jìng)爭(zhēng)鎖),所以可以通過(guò)偏向來(lái)提高性能。即在無(wú)競(jìng)爭(zhēng)時(shí),之前獲得鎖的線程再次獲得鎖時(shí),會(huì)判斷是否偏向鎖指向我,那么該線程將不用再次獲得鎖,直接就可以進(jìn)入同步塊。但是高并發(fā)的情況下,偏向鎖會(huì)經(jīng)常失效,導(dǎo)致需要取消偏向鎖,取消偏向鎖的時(shí)候,需要 Stop the world,因?yàn)橐@取每個(gè)線程使用鎖的狀態(tài)以及運(yùn)行狀態(tài)。
(5)Java Instrument 導(dǎo)致的 Agent 加載以及類(lèi)的重定義:由于涉及到類(lèi)重定義,需要修改棧上和這個(gè)類(lèi)相關(guān)的信息,所以需要 Stop the world
(6)Java Code Cache相關(guān):當(dāng)發(fā)生 JIT 編譯優(yōu)化或者去優(yōu)化,需要 OSR 或者 Bailout 或者清理代碼緩存的時(shí)候,由于需要讀取線程執(zhí)行的方法以及改變線程執(zhí)行的方法,所以需要 Stop the world
5
避免Safepoint副作用
Safepoint在一定程度上是可以理解成是為了讓所有用戶(hù)線程停頓(Stop The World)而設(shè)計(jì)的。STW對(duì)應(yīng)用系統(tǒng)來(lái)說(shuō)是一件很可怕的事情,JVM不論是在GC還是在其他的VM操作上都在努力避免STW和減少STW時(shí)間。
安全點(diǎn)最主要的副作用就是可能導(dǎo)致STW時(shí)間過(guò)長(zhǎng),應(yīng)該極力避免這點(diǎn)副作用。
對(duì)第一個(gè)進(jìn)入安全點(diǎn)的線程來(lái)說(shuō),STW是從它進(jìn)入安全點(diǎn)開(kāi)始的,如果有某個(gè)線程一直無(wú)法進(jìn)入安全點(diǎn)就會(huì)導(dǎo)致進(jìn)入安全點(diǎn)的時(shí)間一直處于等待狀態(tài),進(jìn)而導(dǎo)致STW的時(shí)間過(guò)長(zhǎng)。所以,應(yīng)避免線程執(zhí)行過(guò)長(zhǎng)無(wú)法進(jìn)入安全點(diǎn)的情況。
可數(shù)循環(huán)體內(nèi)執(zhí)行時(shí)間過(guò)長(zhǎng)以及JIT優(yōu)化導(dǎo)致無(wú)法進(jìn)入安全點(diǎn)的問(wèn)題是最常見(jiàn)的無(wú)法進(jìn)入安全點(diǎn)的情況。在寫(xiě)大循環(huán)的時(shí)候可以把循環(huán)索引值數(shù)據(jù)類(lèi)型定義成long。
在高并發(fā)應(yīng)用中,偏向鎖并不能帶來(lái)性能提升,反而因?yàn)槠蜴i取消帶來(lái)了很多沒(méi)必要的某些線程進(jìn)入安全點(diǎn) 。所以建議關(guān)閉:-XX:-UseBiasedLocking。
jstack,jmap 和 jstat 等命令,也會(huì)導(dǎo)致進(jìn)入安全點(diǎn)。所以,生產(chǎn)環(huán)境應(yīng)該關(guān)閉Thead dump的開(kāi)關(guān),避免dump時(shí)間過(guò)長(zhǎng)導(dǎo)致應(yīng)用STW時(shí)間過(guò)長(zhǎng)。
審核編輯:劉清
-
JAVA語(yǔ)言
+關(guān)注
關(guān)注
0文章
138瀏覽量
20116 -
STW
+關(guān)注
關(guān)注
0文章
3瀏覽量
6996 -
JVM
+關(guān)注
關(guān)注
0文章
158瀏覽量
12239 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
919瀏覽量
28288
原文標(biāo)題:深入淺出解析JVM中的Safepoint
文章出處:【微信號(hào):OSC開(kāi)源社區(qū),微信公眾號(hào):OSC開(kāi)源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論