文章目錄
- 寫在文章開頭
- 詳解safepoint基本概念
- 什么是安全點?為什么需要安全點
- JVM如何讓線程跑到最近的安全點
- 線程什么時候需要進入安全點
- JVM如何保證線程高效進入安全點
- 如何設置安全點
- 用一次GC解釋基于安全點的STW
- 實踐-基于主線程休眠了解安全點的工作過程
- 代碼示例
- 基于日志印證執行流程
- 優化思路
- 關于安全點更進一步的理解
- 關于安全點的調優建議
- JDK11對于安全點的優化
- RocketMQ中對于安全點的優化
- 小結
- 參考
寫在文章開頭
近期在分享關于synchronized
關鍵字的文章的時候提到了一個關于安全點的概念,有讀者反饋這塊知識點講的有些潦草,遂以此文簡單介紹一下JVM
中關于安全點的概念。
詳解safepoint基本概念
什么是安全點?為什么需要安全點
在正式講解安全點之前,我們不妨復習一下JVM
中垃圾回收的基本過程,我們以CMS
垃圾回收器為例,其垃圾回收過程在完成GC Roots
查找與收集之后就會按照如下步驟執行:
- 初始標記
- 并發回收
- 最終標記(重新標記)
- 并發清除
要知道固定可作為GC Roots
的節點主要是:
- 全局引用:例如常量或者靜態變量。
- 執行上下文即棧幀中的變量表。
對于現代java
應用而言,光是方法區就可能有數百上千兆,所以對于這些起源的引用也并非一件容易的事情。這也就意味著JVM
在進行垃圾回收時并不能通過逐個掃描檢查來實現。
就目前主流的JVM來說,針對根節點枚舉基本都是采用空間換時間的策略,也就是使用一組OopMap
,全稱為"Object Pointer Map"(對象指針映射)
,本質上就是一個位圖索引,它會通過以下兩個時機完成對象信息的緩存:
- 類加載完成后,
hotSpot
就會基于類的偏移量信息計算出來并緩存。 JIT
階段也會在特定的時機(這一點后續會詳細說明)
計算出棧或寄存器中的那些位置是引用,并將其緩存。
如此一來,下次進行根枚舉時就可以直接基于OopMap
高效完成:
但是java進程的運行的瞬息萬變的,可能此刻的對象在下一刻就不可用,下一刻又有新的對象誕生,這種引用關系的實時變化亦或者說導致OopMap
內容變化的指令是非常多的,若針對每一個指令都設置對應的oopMap
,那么內存的開銷是非常高昂的。
所以就有了安全點(safepoint)
的概念,這也就是我們上文所提及的特定的位置
,基于這個設定,用戶的程序僅僅會在特定的情況下生成oopMap
,同理在垃圾回收時,也要求所有線程達到安全點后才能夠暫停并進入STW
從而開始進行初始標記、最終標記等操作:
例如下面這段代碼:
Object o=new Object();
對應匯編碼如下,可以看到0x00000000031ffb8f
的call
指令,它指明偏移量40-852處有一個普通對象指針Oop(Ordinary Object Pointer)
:
0x00000000031ffb80: mov $0xf5,%edx0x00000000031ffb85: mov %ecx,%ebp0x00000000031ffb87: mov %rbx,0x28(%rsp)0x00000000031ffb8c: data16 xchg %ax,%ax0x00000000031ffb8f: callq 0x00000000030957a0 ; OopMap{[40]=Oop off=852};*new ; - java.lang.String::<init>@58 (line 205); - java.lang.String::substring@52 (line 1933); {runtime_call}
JVM如何讓線程跑到最近的安全點
對于安全點上的線程中斷策略,大體來說是有兩種:
- 搶占式:當需要進入安全點時,
JVM
會主動掛起所有的用戶線程,如果線程未在安全點則等到該線程進入安全點進入安全點并完成中斷。這種做法最大的缺點就是時間不可控即很可能存在性能不穩定亦或者吞吐量的波動,所以截至目前還有那款虛擬機采用搶占式的方式完成線程中斷。 - 主動式:這種方式是讓線程去維護一個標志位,需要進入安全點時修改該變量,用戶線程就會在合適的時機檢查這個變量值,如果這個值為真時就進入安全點。
線程什么時候需要進入安全點
除了常見的垃圾回收標記觸發STW使得所有線程需要進入安全點以外,對應的進入安全點的時機還有:
- 使用
jstat
、jmap
、jstack
等命令,為保證監控堆棧信息的實時正確性,所有線程需要STW并進入安全點暫停。 - JDK8默認情況下定時進入安全點,保證一些需要進入安全點的操作能夠及時運行。
- JIT編譯代碼優化例如:OSR(棧上替換即一種運行時替換棧幀的技術)或者去優化即Bailout(將JIT編譯后的代碼回退,解釋器模式),因為可能存在執行指令的變化,線程就需要進入安全點。
- java agent需要對類進行增強導致類重新定義,需要修改類的相關信息,所以需要進入安全點。
- 高并發情況下,鎖升級機制會涉及偏向鎖撤銷,需要進入STW所以也需要進入安全點。
JVM如何保證線程高效進入安全點
我們以線程運行JIT編譯好的代碼為例,它的設計與實現步驟為:
- JVM初始化一個異常處理器,專門捕獲對應的
page fault
缺頁中斷異常。 - JIT編譯代碼期間,會基于我們上述的規則在特定位置插入一條精簡的指令,作為安全點檢查。
- VM線程通知當前線程進入安全點,將線程內部維護的內存頁即
polling page
設置為不可讀。 - 線程執行這條機器碼指令發現內存頁不可讀,觸發缺點中斷。
- 異常處理器捕獲這個異常,線程進入安全點。
對應的我們也給出這段精簡的匯編碼指令,即test %eax,0x160100 ; {poll}
這段指令,這段指令本質上就是執行poll操作檢查安全點,嘗試訪問線程內存頁對應地址為0x160100
,如果發現不可訪問則觸發缺頁中斷進入安全點:
0x01b6d627: call 0x01b2b210 ;<