文末 JVM 思維導圖,有需要的可以自取
熟知併發程式設計的你認為下面這段程式碼的執行結果是怎麼樣的?
我如果說,執行流程是:
- t1 執行緒和 t2 執行緒一直執行 num 的累加操作
- 主執行緒睡眠 1 秒,1 秒之後醒過來列印此時的 num 值
- t1 執行緒和 t2 執行緒繼續執行加 1 的操作,直到執行完 2億 次累加操作
你贊成嗎?
我的猜想看起來沒什麼問題,但實際執行效果證明了我是錯的,下面是執行動圖:
從執行動圖上可以看到,將程式碼跑起來之後,卻發現實際執行結果是這樣的:
1 秒之後,主執行緒並沒有馬上列印 num,而是等 t1 和 t2 分別執行完 2 億次累加操作退出迴圈後,才會列印 num 的值。
這個結果和預想的不一樣。我是基於 JDK1.8 跑的,你也可以試試。
為什麼會這樣呢?
答案是:
JVM 想要執行某個操作,讓所有執行緒進入安全點,但是 t1 和 t2 執行緒因為 JIT 對可數迴圈的過渡優化必須等迴圈跑完了才進入安全點,所以主執行緒一直再等 t1 和 t2,遲遲不能輸出 num 的值。
可數迴圈:形如 for (int i = 0; i < 100000000; i++) {...}的迴圈被稱為可數迴圈
簡單來說就是:主執行緒在等 t1 和 t2 執行緒進入安全點
這個答案的由來,why 神轉載的一篇文章:《真是絕了!這段被 JVM 動了手腳的程式碼!》中已經說的很清楚了,這裡不再重複闡述。
此文就源於我當時的一個疑問:JVM 讓執行緒都進入安全點到底幹了什麼不為人知的事情?
發生了 GC?
難道是發生了 GC 嗎?
第一,程式碼裡面沒有建立物件申請記憶體。
第二,加上 -XX:-PrintGC 也沒有列印 GC 日誌。
第三,執行 jstat 命令,通過輸出日誌可以看出,JVM 執行期間各個記憶體區域都沒有發生變化,也沒有發生 GC。
所以,因為發生了 GC 而需要進入安全點這種情況被排除了。
問題就變成了:沒有發生 GC,需要所有的執行緒都進入安全點幹什麼?
安全點日誌
加上 -XX:+PrintSafepointStatistics 引數,讓程式執行的時候列印安全點的相關日誌。
可以看到,這段程式碼的執行一共進行了三次進入安全點。
其中第二個 EnableBiasedLocking 是 JVM 延時開啟偏向鎖的操作,這個也比較有意思,不過不是文章的重點,下次有機會再說。
我們重點關注的是第一個 no vm operation 操作。將這段日誌單獨拿出來,在引數說明上加上中文解釋:
總結來說就是:
JVM 想執行 no vm operation ,這個操作需要執行緒都進入安全點,整個期間一共有 12 個執行緒,正在執行的執行緒有 2 個,需要等待這兩個執行緒進入安全點,等待這 2 個執行緒進入安全點並阻塞耗費了 5037 毫秒。
要找出這兩個執行緒也很簡單,它不是需要 5000 多毫秒才進入安全點嗎,我就加上引數讓進入安全點時間超過 5000 毫秒的執行緒超時就行了。
於是加上 -XX:+SafepointTimeout 和 -XX:SafepointTimeoutDelay=5000 引數,執行程式碼。
哦豁,這不就是 t1 和 t2 執行緒嗎。
這個結果也是意料之中的,我們的重點是這個 no vm operation 到底是個什麼操作?憑什麼讓主執行緒等這麼久?
原始碼定位
這個 VM 操作的名字叫做 no vm operation ,翻譯成中文就是不是 VM 操作,連起來就是不是 VM 操作的 VM 操作?
一個不是 VM 操作的操作居然也能讓全域性進入安全點?
那到底是什麼操作呢?知識盲區了呀!
一頓谷歌百度,也沒有找到一個比較信服的答案。
於是乎,我決定看 JVM 的原始碼。
在 JVM 原始碼裡面全域性搜尋 no vm operation ,發現只有 safepoint.cpp 有這個資訊。
點選去一看,果然,一下子定位到列印日誌的地方,就是這個 SafepointSynchronize::print_statistics() 方法。
其中有一句很關鍵的程式碼:
_vmop_type == -1 ?
"no vm operation" :
VM_Operation::name(sstats->_vmop_type)
這是一個三目運算:如果 _vmop_type 等於 -1,列印的安全點日子操作型別那一欄就會輸出 no vm operation 。
而這個 _vmop_typen 呢,是結構體 SafepointStats 中的一個成員,具體的含義是觸發安全點的 VM 操作型別。
那什麼操作型別會將 _vmop_type 設定成 -1 呢?
我在開啟安全點方法裡面找到了答案:
如果不是 VM 操作觸發的安全點事件,這個時候就會將 _vmop_type 設定成 -1。
也就是說還有其他情況也可以觸發安全點事件,讓所有執行緒進入安全點。
那麼,我們只需要找到觸發安全點事件對應的程式碼就行了。
一個個檔案找太難,換個思路,想要進入安全點,必定要呼叫進入安全點的方法。
而進入安全點的方法就是 safepoint.cpp 裡面的 SafepointSynchronize::begin() 方法。
我們只需要全域性搜一下哪裡呼叫了這個 SafepointSynchronize::begin() 這個方法應該就能找到觸發安全點事件對應的程式碼。
全域性搜尋發現只有 vmThread.cpp 裡面有呼叫,vmThread.cpp 封裝的都是 VMThread 相關的方法。
VMThread
VMThread 是個什麼東西呢?
VMThread 是 JVM 自身啟動的一個內部執行緒,它主要用來協調其它執行緒達到安全點以及執行 VM 操作。
VM 操作這個概念全文已經多次提到了,那到底有哪些操作是 VM 操作呢?
我們比較熟悉的 CMS 的初始標記和最終標記都是 VM 操作,又比如 thread dump,執行緒掛起以及偏向鎖的撤銷等等都是 VM 操作。
VM 操作型別有很多,JVM 對應的原始碼在 vm_operations.hpp 定義的巨集 VM_OPS_DO 裡面。
巨集 VM_OPS_DO 裡面的每個 VM 操作,基本上都有一個單獨的子類去實現。
VMThread 裡面有個 VMOperationQueue 佇列,用於存放一個一個連在一起的 VM 操作。
VMThread 迴圈執行 VM 操作的方法,叫做 VMThread::loop() 方法。
loop() 方法是 VMThread 的核心方法,該方法不斷從 VMOperationQueue 佇列中獲取待執行的 VM 操作,然後呼叫每種 VM 操作具體的實現 evaluate() 方法執行不同的邏輯。
這裡用了策略模式,VMThread 執行邏輯是固定的,只負責排程,而每種 VM 操作需要根據需求自己實現 evaluate() 方法。
答案出現
而我們上面苦苦尋找的 no vm operation 原因,就在 VMThread 的 loop() 方法裡面。
從原始碼可以看到,在 VM 操作為空的情況下,只要滿足以下 3 個條件,也是會進入安全點的:
- VMThread 處於正常執行狀態
- 設計了進入安全點的間隔時間
- SafepointALot 是否為 true 或者是否需要清理
程式正常執行 VMThread 肯定能正常執行,所以條件 1 能滿足。
用 java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal 2>&1 | grep Safepoint 命令檢視 JVM 關於安全點的預設引數,發現 GuaranteedSafepointInterval 預設設定成了 1 秒,所以條件 2 也能滿足。
對於條件 3,SafepointALot 預設為 false,那要想條件 3 能滿足的話,必須 SafepointSynchronize::is_cleanup_needed()為 true。
點進去看它的具體實現:
通過追蹤程式碼,可以發現 SafepointSynchronize::is_cleanup_needed() 就是判斷 StubQueue 裡面是否有 stub 快取。
那 StubQueue 是什麼呢?stub 又是什麼呢?
這涉及 JVM 的模板直譯器和編譯器了,由於篇幅有限,下次有機會的話繼續深入探討。
我用一句話概括就是 JVM 執行期間的編譯解釋程式碼快取。
清理 stub 你可以簡單的理解成清理程式碼快取。
也就是說,在 JVM 正常執行的時候,如果設定了進入安全點的間隔,就會隔一段時間判斷是否有程式碼快取要清理,如果有,會進入安全點。
這個觸發條件不是 VM 操作,所以會將 _vmop_type 設定成-1,輸出日誌的時候列印對應的 no vm operation,也就是我們看到的安全點日誌。
而文章開頭的程式碼執行效果,主執行緒一直在等待 t1 和 t2 進入安全點,正是觸發了這個條件。
再次驗證推論
回過頭來再看文章開頭的程式碼,通過加上 -XX:GuaranteedSafepointInterval = 0 將進入安全點間隔時間設定成 0,也就是關閉定時進入安全點,看看程式碼執行結果是怎麼樣的。
-XX:GuaranteedSafepointInterval 是診斷性質的引數,需要加上-XX:+UnlockDiagnosticVMOptions 引數解鎖診斷引數方可使用。
從執行結果上可以看到,關閉過一段時間進入安全點的設定之後,主執行緒睡了 1 秒後,不再需要等待 t1 和 t2 執行緒迴圈執行完,睡完之後馬上就列印了此時的 num 值。
這樣的執行結果,也再一次的驗證了我們的推論。
間隔一秒進入安全點的設定還是有它的作用的,我建議你別去動它。
-XX:GuaranteedSafepointInterval 是個診斷性質的引數,不建議線上使用。
從網上的文獻來看,關掉這個引數也有可能會造成一些未知錯誤,具體是什麼錯誤我也沒有遇見過,也不知道是真是假。
總之,線上環境謹慎一點總沒錯,如果你對 JVM 底層不是很熟悉的話,我建議還是別去動它。
有趣的註釋
知識點分享到這裡就結束了,分享一個有趣的事情。
在我追蹤 JVM 原始碼的過程中,我發現編寫 StubQueue 的作者留下了這樣一段註釋:
我潤色翻譯一下就是:在你不能證明你改的沒問題的時候,別特麼亂動我程式碼,這段程式碼比你想象中牛逼的多。
看到沒有,這就是大神的驕傲和自信!
反觀我呢,我平時給程式碼寫註釋的時候,只敢在上面寫:如果你看到我的程式碼有 BUG,麻煩幫我修一下,謝謝了。
從寫註釋的驕傲和自信上就能看得出,我和大神差距有多大了。
我一定要加油,以後也能寫出這樣霸氣的註釋!
思維導圖
我把我個人覺得重要的 JVM 知識點,按照自己理解思路整理成了一個思維導圖。
有需要的可以自取就行,如果圖片被平臺壓縮了,你可以公眾號後臺回 JVM 獲取高清圖片。
需要強調的是,這是我整理的知識點,裡面的知識並不是我原創的。
我沒有創造知識,只是分享自己如何學習和理解知識。
思維導圖的製作參照了大量的書籍和部落格,包括但不限於《深入理解 Java 虛擬機器》、美團技術團隊文章、阿里技術團隊文章、R 大的文章、寒泉子大大的調優文章。
好了,今天的文章就到此結束了。
我是 CoderW,一個有時候喜歡鑽牛角尖的程式設計師,我們下期再見!