<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我被讀者卷了...

          共 5085字,需瀏覽 11分鐘

           ·

          2021-05-19 13:50


          8a4aa426a0dea3ceb9e952dbd81ffa7f.webp上海外灘 | 攝影?阿強

          從一個問題開始,熟知并發(fā)編程的你認(rèn)為下面這段代碼的執(zhí)行結(jié)果是怎么樣的?

          26a71f388dd7ea6a4c95a6dd13914fd0.webp

          我如果說,執(zhí)行流程是:

          1. t1 線程和 t2 線程一直執(zhí)行 num 的累加操作
          2. 主線程睡眠 1 秒,1 秒之后醒過來打印此時的 num 值
          3. t1 線程和 t2 線程繼續(xù)執(zhí)行加 1 的操作,直到執(zhí)行完 2億次累加操作

          你贊成嗎?

          我的猜想看起來沒什么問題,但實際運行效果證明了我是錯的,下面是運行動圖:

          71914aa0376c485872b628f44dbdae85.webp

          從運行動圖上可以看到,將代碼跑起來之后,卻發(fā)現(xiàn)實際執(zhí)行結(jié)果是這樣的:

          1 秒之后,主線程并沒有馬上打印 num,而是等 t1 和 t2 分別執(zhí)行完 2 億次累加操作退出循環(huán)后,才會打印 num 的值。

          這個結(jié)果和預(yù)想的不一樣。我是基于 JDK1.8 跑的,你也可以試試。

          為什么會這樣呢?

          答案是:

          JVM 想要執(zhí)行某個操作,讓所有線程進(jìn)入安全點,但是 t1 和 t2 線程因為 JIT 對可數(shù)循環(huán)的過渡優(yōu)化必須等循環(huán)跑完了才進(jìn)入安全點,所以主線程一直再等 t1 和 t2,遲遲不能輸出 num 的值。

          可數(shù)循環(huán):

          形如 for (int i = 0; i < 100000000; i++) {...}的循環(huán)被稱為可數(shù)循環(huán)

          簡單來說就是:「主線程在等 t1 和 t2 線程進(jìn)入安全點」

          這個答案的由來,why 哥的一篇文章:《真是絕了!這段被 JVM 動了手腳的代碼!》中已經(jīng)說的很清楚了,這里不再重復(fù)闡述。

          此文就源于我當(dāng)時的一個疑問:JVM 讓線程都進(jìn)入安全點到底干了什么不為人知的事情?

          發(fā)生了 GC?

          難道是發(fā)生了 GC 嗎?

          第一,代碼里面沒有創(chuàng)建對象申請內(nèi)存。

          第二,加上 -XX:+PrintGC 也沒有打印 GC 日志。

          第三,執(zhí)行 jstat 命令,通過輸出日志可以看出,JVM 運行期間各個內(nèi)存區(qū)域都沒有發(fā)生變化,也沒有發(fā)生 GC。

          5b801feace118bd9155d7dae14f4a35d.webp

          所以,因為發(fā)生了 GC 而需要進(jìn)入安全點這種情況被排除了。

          問題就變成了:沒有發(fā)生 GC,需要所有的線程都進(jìn)入安全點干什么?

          安全點日志

          加上 -XX:+PrintSafepointStatistics 參數(shù),讓程序執(zhí)行的時候打印安全點的相關(guān)日志。

          e8e5f8ac28c5fc8291888f90c0e99fea.webp

          可以看到,這段代碼的執(zhí)行一共進(jìn)行了三次進(jìn)入安全點。

          其中第二個 EnableBiasedLocking 是 JVM 延時開啟偏向鎖的操作,這個也比較有意思,不過不是文章的重點,下次有機會再說。

          我們重點關(guān)注的是第一個 「no vm operation」 操作。將這段日志單獨拿出來,在參數(shù)說明上加上中文解釋:

          cc3f4f78e6a79010f8f630007e7ae4b0.webp

          總結(jié)來說就是:

          JVM 想執(zhí)行 「no vm operation」 ,這個操作需要線程都進(jìn)入安全點,整個期間一共有 12 個線程,正在運行的線程有 2 個,需要等待這兩個線程進(jìn)入安全點,等待這 2 個線程進(jìn)入安全點并阻塞耗費了 5037 毫秒。

          要找出這兩個線程也很簡單,它不是需要 5000 多毫秒才進(jìn)入安全點嗎,我就加上參數(shù)讓進(jìn)入安全點時間超過 5000 毫秒的線程超時就行了。

          于是加上 -XX:+SafepointTimeout 和 -XX:SafepointTimeoutDelay=5000 參數(shù),執(zhí)行代碼。

          02e727b5e3fff74167172c48079c14fc.webp

          哦豁,這不就是 t1 和 t2 線程嗎。

          這個結(jié)果也是意料之中的,我們的重點是這個 「no vm operation」 到底是個什么操作?憑什么讓主線程等這么久?

          源碼定位

          這個 VM 操作的名字叫做 「no vm operation」 ,翻譯成中文就是「不是 VM 操作」,連起來就是不是 VM 操作的 VM 操作?

          12fc82a66f98909703bcc0959011bedf.webp

          一個不是 VM 操作的操作居然也能讓全局進(jìn)入安全點?

          那到底是什么操作呢?知識盲區(qū)了呀!

          一頓谷歌百度,也沒有找到一個比較信服的答案。

          于是乎,我決定看 JVM 的源碼。

          在 JVM 源碼里面全局搜索 「no vm operation」 ,發(fā)現(xiàn)只有 safepoint.cpp 有這個信息。

          6631ddff2f210860eecc0cedf13da280.webp

          點擊去一看,果然,一下子定位到打印日志的地方,就是這個 SafepointSynchronize::print_statistics() 方法。

          f6d847178288c97e9e9f21fad0f2e26a.webp

          其中有一句很關(guān)鍵的代碼:

          _vmop_type?==?-1???
          ????"no?vm?operation"?:?
          ????VM_Operation::name(sstats->_vmop_type)

          這是一個三目運算:如果 _vmop_type 等于 -1,打印的安全點日子操作類型那一欄就會輸出 「no vm operation」

          而這個 _vmop_typen 呢,是結(jié)構(gòu)體 SafepointStats 中的一個成員,具體的含義是:觸發(fā)安全點的 VM 操作類型。

          7f6be9dde532b30feb8a8a29b11e1105.webp

          那什么操作類型會將 _vmop_type 設(shè)置成 -1 呢?

          我在開啟安全點方法里面找到了答案:

          e5af59d3a45f0862f3e20f173f538189.webp

          如果不是 VM 操作觸發(fā)的安全點事件,這個時候就會將 _vmop_type 設(shè)置成 -1。

          也就是說還有其他情況也可以觸發(fā)安全點事件,讓所有線程進(jìn)入安全點。

          那么,我們只需要找到觸發(fā)安全點事件對應(yīng)的代碼就行了。

          一個個文件找太難,換個思路,想要進(jìn)入安全點,必定要調(diào)用進(jìn)入安全點的方法。

          而進(jìn)入安全點的方法就是 safepoint.cpp 里面的 SafepointSynchronize::begin() 方法。

          b33970c29936456c271b22996fc88a71.webp

          我們只需要全局搜一下哪里調(diào)用了這個 SafepointSynchronize::begin() 這個方法應(yīng)該就能找到觸發(fā)安全點事件對應(yīng)的代碼。

          42cc42365f559d63180293d09cb2c20b.webp

          全局搜索發(fā)現(xiàn)只有 vmThread.cpp 里面有調(diào)用,vmThread.cpp 封裝的都是 VMThread 相關(guān)的方法。

          VMThread

          VMThread 是個什么東西呢?

          VMThread 是 JVM 自身啟動的一個內(nèi)部線程,它主要用來協(xié)調(diào)其它線程達(dá)到安全點以及執(zhí)行 VM 操作。

          VM 操作這個概念全文已經(jīng)多次提到了,那到底有哪些操作是 VM 操作呢?

          我們比較熟悉的 CMS 的初始標(biāo)記和最終標(biāo)記都是 VM 操作,又比如 thread dump,線程掛起以及偏向鎖的撤銷等等都是 VM 操作。

          VM 操作類型有很多,JVM 對應(yīng)的源碼在 vm_operations.hpp 定義的宏 VM_OPS_DO 里面。

          be99bad6d0d8cea98748f1fe1fe9ca95.webp

          宏 VM_OPS_DO 里面的每個 VM 操作,基本上都有一個單獨的子類去實現(xiàn)。

          VMThread 里面有個 VMOperationQueue 隊列,用于存放一個一個連在一起的 VM 操作。

          a2c9689f043960e5018fa2730afe5b07.webp

          VMThread 循環(huán)執(zhí)行 VM 操作的方法,叫做 VMThread::loop() 方法。

          loop() 方法是 VMThread 的核心方法,該方法不斷從 VMOperationQueue 隊列中獲取待執(zhí)行的 VM 操作,然后調(diào)用每種 VM 操作具體的實現(xiàn) evaluate() 方法執(zhí)行不同的邏輯。

          這里用了策略模式,VMThread 執(zhí)行邏輯是固定的,只負(fù)責(zé)調(diào)度,而每種 VM 操作需要根據(jù)需求自己實現(xiàn) evaluate() 方法。

          答案出現(xiàn)

          而我們上面苦苦尋找的 no vm operation 原因,就在 VMThread 的 loop() 方法里面。

          d87cd90c4d52bfec3ea2a594f02a0f7d.webp

          從源碼可以看到,在 VM 操作為空的情況下,只要滿足以下 3 個條件,也是會進(jìn)入安全點的:

          1. VMThread 處于正常運行狀態(tài)
          2. 設(shè)計了進(jìn)入安全點的間隔時間
          3. SafepointALot 是否為 true 或者是否需要清理

          程序正常運行 VMThread 肯定能正常運行,所以條件 1 能滿足。

          「java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint」 命令查看 JVM 關(guān)于安全點的默認(rèn)參數(shù),發(fā)現(xiàn) GuaranteedSafepointInterval 默認(rèn)設(shè)置成了 1 秒,所以條件 2 也能滿足。

          8996d238b7c29826ce34b3f8c1806510.webp

          對于條件 3,SafepointALot 默認(rèn)為 false,那要想條件 3 能滿足的話,必須 SafepointSynchronize::is_cleanup_needed()為 true。

          點進(jìn)去看它的具體實現(xiàn):

          50b2abf598df9928ab024a19f90d97ca.webp

          通過追蹤代碼,可以發(fā)現(xiàn) SafepointSynchronize::is_cleanup_needed() 就是判斷 StubQueue 里面是否有 stub 緩存。

          那 StubQueue 是什么呢?stub 又是什么呢?

          這涉及 JVM 的模板解釋器和編譯器了,由于篇幅有限,下次有機會的話繼續(xù)深入探討。

          我用一句話概括就是 ?「JVM 執(zhí)行期間的編譯解釋代碼緩存」

          清理 stub 你可以簡單的理解成「清理代碼緩存」

          也就是說,在 JVM 正常運行的時候,如果設(shè)置了進(jìn)入安全點的間隔,就會隔一段時間判斷是否有代碼緩存要清理,如果有,會進(jìn)入安全點。

          這個觸發(fā)條件不是 VM 操作,所以會將 _vmop_type 設(shè)置成-1,輸出日志的時候打印對應(yīng)的 「no vm operation」,也就是我們看到的安全點日志。

          而文章開頭的代碼執(zhí)行效果,主線程一直在等待 t1 和 t2 進(jìn)入安全點,正是觸發(fā)了這個條件。

          再次驗證推論

          回過頭來再看文章開頭的代碼,通過加上 -XX:GuaranteedSafepointInterval = 0 將進(jìn)入安全點間隔時間設(shè)置成 0,也就是關(guān)閉定時進(jìn)入安全點,看看代碼運行結(jié)果是怎么樣的。

          -XX:GuaranteedSafepointInterval 是診斷性質(zhì)的參數(shù),需要加上-XX:+UnlockDiagnosticVMOptions 參數(shù)解鎖診斷參數(shù)方可使用。

          e48098aa2330f691cce2ba80df4c7fed.webp

          從運行結(jié)果上可以看到,關(guān)閉過一段時間進(jìn)入安全點的設(shè)置之后,主線程睡了 1 秒后,不再需要等待 t1 和 t2 線程循環(huán)執(zhí)行完,睡完之后馬上就打印了此時的 num 值。

          這樣的運行結(jié)果,也再一次的驗證了我們的推論。

          間隔一秒進(jìn)入安全點的設(shè)置還是有它的作用的,我建議你別去動它。

          -XX:GuaranteedSafepointInterval 是個診斷性質(zhì)的參數(shù),不建議線上使用。

          從網(wǎng)上的文獻(xiàn)來看,關(guān)掉這個參數(shù)也有可能會造成一些未知錯誤,具體是什么錯誤我也沒有遇見過,也不知道是真是假。

          總之,線上環(huán)境謹(jǐn)慎一點總沒錯,如果你對 JVM 底層不是很熟悉的話,我建議還是別去動它。

          有趣的注釋

          知識點分享到這里就結(jié)束了,分享一個有趣的事情。

          在我追蹤 JVM 源碼的過程中,我發(fā)現(xiàn)編寫 StubQueue 的作者留下了這樣一段注釋:

          2d50367c3e6feb663fefe09d03dd7350.webp

          我潤色翻譯一下就是:「在你不能證明你改的沒問題的時候,別特么亂動我代碼,這段代碼比你想象中牛逼的多」

          看到?jīng)]有,這就是大神的驕傲和自信!

          反觀我呢,我平時給代碼寫注釋的時候,只敢在上面寫:「如果你看到我的代碼有 BUG,麻煩幫我修一下,謝謝了」

          e86c43712e5e47e92367e7b99ecbecf9.webp

          從寫注釋的驕傲和自信上就能看得出,我和大神差距有多大了。

          我一定要加油,以后也能寫出這樣霸氣的注釋!

          思維導(dǎo)圖

          我把我個人覺得重要的 JVM 知識點,按照自己理解思路整理成了一個思維導(dǎo)圖。

          e3bdd05910767997fa56acdc5a80a0d4.webp

          有需要的可以自取就行,如果圖片被平臺壓縮了,你可以公眾號后臺回 ?「JVM」 ?獲取高清圖片。

          注意:是去這個號下面回復(fù)「JVM」獲取高清圖片!!!

          需要強調(diào)的是,這是我整理的知識點,里面的知識并不是我原創(chuàng)的。

          我沒有創(chuàng)造知識,只是分享自己如何學(xué)習(xí)和理解知識。

          思維導(dǎo)圖的制作參照了大量的書籍和博客,包括但不限于《深入理解 Java 虛擬機》、美團(tuán)技術(shù)團(tuán)隊文章、阿里技術(shù)團(tuán)隊文章、R 大的文章、寒泉子大大的調(diào)優(yōu)文章。

          好了,今天的文章就到此結(jié)束了。

          why哥說

          這篇文章的作者也是我的讀者,當(dāng)時我發(fā)布了《真是絕了!這段被 JVM 動了手腳的代碼!》這篇文章之后,他就和我進(jìn)行了深入的交流,直接把 JVM 的源碼貼給我看。

          a31436b8e5e454a3839cc077801fa5ea.webp

          我當(dāng)時就覺得:這小伙子是要卷我啊?不能慫,開始對線。

          于是發(fā)了一個表情包:

          de89d5356c82399ca5cea9619b1420b6.webp

          對于這種一言不合就開始卷 JVM 源碼的,我是真的卷不過了:

          d4f9aa83dcf0d8ee794d0c0c89f40a51.webp

          最后還不忘給我撒把狗糧:

          b26d03b1b9f8590cd8a4edf75cac842a.webp


          其實關(guān)于之前文章中寫的安全點的那個案例,在發(fā)布沒幾天后,我剛好在《Java性能優(yōu)化實戰(zhàn)》一書的 13.2 小節(jié)采樣與安全點偏差,看到了這樣的一段描述:

          b1a26c2cc9600b05219d866beb9fa923.webp

          害,原來答案寫的到處都是,只是我還沒翻到而已。所以還是得讀書,不說了,我要上分...哦不,我要讀書了。

          542eb9792b8a7345de6010c31d2ee519.webp



          推薦???:面試官:啥是請求重發(fā)啊?推薦???:編程玄學(xué),走近科學(xué)推薦???:該不該裸辭?推薦???:老爺子這代碼,看跪了!推薦???:這個Bug的排查之路,真的太有趣了。

          轉(zhuǎn)發(fā)、點贊、在看、一鍵三連。

          315d3b6defba614092962e914036a5fb.webp
          瀏覽 133
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产99热 | 波多野吉衣AⅤ无码一区小说 | 夜夜操综合网二区 | 美国富婆吃鸡巴视频 | 亚洲国产97 |