文末 JVM 思惟導圖,有須要的自取
熟知併發編程的你認爲下面這段代碼的執行結果是怎麼樣的?java
我若是說,執行流程是:c++
你同意嗎?程序員
個人猜測看起來沒什麼問題,但實際運行效果證實了我是錯的,下面是運行動圖:編程
從運行動圖上能夠看到,將代碼跑起來以後,卻發現實際執行結果是這樣的:緩存
1 秒以後,主線程並無立刻打印 num,而是等 t1 和 t2 分別執行完 2 億次累加操做退出循環後,纔會打印 num 的值。安全
這個結果和預想的不同。我是基於 JDK1.8 跑的,你也能夠試試。併發
爲何會這樣呢?oop
答案是: 學習
JVM 想要執行某個操做,讓全部線程進入安全點,可是 t1 和 t2 線程由於 JIT 對可數循環的過渡優化必須等循環跑完了才進入安全點,因此主線程一直再等 t1 和 t2,遲遲不能輸出 num 的值。優化
可數循環:形如 for (int i = 0; i < 100000000; i++) {...}的循環被稱爲可數循環
簡單來講就是:主線程在等 t1 和 t2 線程進入安全點
這個答案的由來,why 神轉載的一篇文章:《真是絕了!這段被 JVM 動了手腳的代碼!》中已經說的很清楚了,這裏再也不重複闡述。
此文就源於我當時的一個疑問:JVM 讓線程都進入安全點到底幹了什麼鮮爲人知的事情?
難道是發生了 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 是 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 確定能正常運行,因此條件 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,一個有時候喜歡鑽牛角尖的程序員,咱們下期再見!