- Hotstpot safepoint介紹
1、什麼是Safepoint
在hotstpot內部,有時候它會把 Java線程暫停掉,有時候又會把它叫做Stop The World,在hotstpot裡可以做很多vm級別操作,如 GC、HeapDump/Stack trace、JVMTI、Check vmOperations.hpp,這裡列了一個 vmOperations.hpp這個頭文件裡面列出了絕大部分的這些vm operation。下圖演示,如正常的java的線程,運行的過程中,有一個VMthread,有些特殊的條件,觸發了一個vm的操作請求,這時候就會發起一個請求,要求Java thread都進入safepoint, Java thread收到請求以後,會自己暫停,等所有的Java thread停下來,整個JVMTI都安全了,可以做一些比較複雜的vm的操作,等操作做完了以後,就可以要求這些Java線程再重新恢復。
舉例來說像GC會把在Heap中的Java對象移來移去,如果這時Java線程正在運行的時候,一邊運行對象一邊移動,Java線程有可能就會訪問到一個非法的地址,造成整個JVMTI的crash,所以這時候需要進入safepoint,把整個Java線程給暫停, Stop The World,會很影響性能。
2、Safepoint中還會做什麼
從上述那些操作可知,在hotstpot中會做很很多事,平均下來,也許一秒鐘之內會有兩三次都會進入到一個safepoint,所以hotstpot會借用這個機會,用safepoint做一些常規的一些清理工作。
舉例,如有些空的monitor,他覺得可以回收了,就可以把它回收到一個monitor的list,還有與inline cache相關的,會把它更新或者是清理掉。
還有些內部數據 stringtable或者symbol table這類數據結構,在safepoint中覺得可以有必要做一些rehash的操作的話也會在這裡做,這些都是一些很短的操作,一般來說並不是特別需要關心,這裡主要提一下,在進入safepoint的時候,hotstpot除了做vm operation以外,還會做一些這種常規的動作。
3、對Safepoint我們關注那些指標
safepoint會把整個jvm的那些應用線程給暫停掉這裡主要是關心的當vm thread發出請求的時候,Java的實驗者能夠及時的響應safepoint的請求,能夠馬上的自己給停下來,如果有一些線程它停下來了,另一些線程還在運行,這樣的話其他的線程就會等於是浪費時間在等待,所以說及時響應是它一個很重要的指標。
進入了safepoint後, vm operation它本身操作,也希望能夠在儘快短的時間內完成,完成了以後,還要能夠快速的退出,這裡一般沒有問題,因為safepoint的退出都比較簡單,一般來說不太會造成什麼影響,前面三個點從進到做vm operation和退出,整個是影響了一次暫停的時間,如果你業務方比較關心這種延遲、響應時間這些指標的話,也許就要關注這幾個性能。
有可能進一次safepoint很短,很快,但是safepoint的發生的時間頻率又很高,這樣的話,就會發現它總體暫停的時間就會很長,所以頻率和總體時間也是一個需要關注的指標,如果對應用的吞吐量和性能比較關注的話,就要關注safepoint的總的暫停時間和它的那些頻率,這裡就是對safepoint有可能要關注一些性能。
4、Safepoint內部實現
safepoint採用的是一種協作式的方式,就是當它發起了safepoint的請求後,那些Java線程來檢測這個請求,然後再把自己給暫停,而不是通過強迫式,例如VMthread調用某一個API強行把一個thread給佔進,強行暫停也許可以很快的把種線程給暫停住,但是這裡會有很多不確定的狀態在裡面,安全性就很容易形成問題。
Hotspot是所以就採用了這種協作式的方式,每個Java線程它能夠及時的判斷出來 safepoint的請求,能夠到一個他自認為可以安全的一個點上把自己給停下來。
與此同時,既然是協作式,就是說這些Java線程怎樣能夠確保它能夠及時的響應,因為有可能在做自己很複雜的業務邏輯,什麼時候去檢查 safepoint,做這麼多的檢查,會不會影響到 Java本身的性能,這些都是需要綜合考慮的一件事。
5、Java thread狀態轉換
在Hotspot裡,對於這種Java的線程,其實主要有三個狀態,在互相這樣轉換,第一個就是說是Thread in Java,這個是說明這個線程現在執行的代碼是Java的代碼,如下圖中標註,在執行Java代碼中,在hotstpot裡它其實又分成兩種模式,一種是解釋器模式就interpreter,第二種是JIT,生成的那種native的code,這兩種模式它在這個裡面處理也是不一樣的。
另外兩了狀態Thread in native和Thread in VM,他們其實執行的都是類似於像c和c++的一些代碼。
Thread in vm的話主要是hotspot本身自己的那些代碼;
Thread initiative的話主要是一些JMI,如Java code有的時候需要調一些GMI的接口去訪問,去調用一些c的庫和方法,這時候它會進入的是Thread in native的狀態。
以上就是他的三個狀態,在 safepoint的時候,要針對這三種不同的情況來做不同的處理。
6、Thread in vm
Thread in vm主要執行的像hotstpot內部代碼,如arraycopy,如現在要執行一個arraycopy拷貝到一半的時候,GC如果把array移到另一個位置,肯定就出問題了,拷貝的都是一個非法的數據,做arraycopy的時候,其實是會把自己Java線程的狀態標誌為Thread in VM,類似的像反射,有的時候做一些resolve或link,hottsport裡有很多的這種操作,因為這些動作它往往是直接去操作hot stpot內部的那些數據結構,所以不會希望有一些vm operation類似像GC那些動作,來做這些事情,所以需要用把線程狀態標誌為Thread in VM,在Thread in VM的狀態下,這個時候VM thread必須要等這個操作給做完以後才能做,所以hot stpot裡對這些在VM狀態的代碼,其實做得很小心,它必須要保證這些這些事情能夠很快的完成,不會有這種長時間的阻塞或者這樣的動作。
7、Thread in native
Thread in native其實是通過JMI接口去執行了 c和c++的一些native的code,在這種狀態下,其實在JMI中已經認為它進入了safepoint,即使已經在運行,與前面提到的stop the world好像理解上有點不一樣,這時候這個線程其實還是可以一直在運行的,因為如果這個代碼是native的code,其實hotstpot是沒法知道是什麼狀態的,而且也沒法控制行為,有可能在做一個很長的 Loop,在那裡不停的執行,所以這個時候如果要等的話,肯定會出問題safepoint就進不去了,但這時候認為已經是safepoint了,就可以做那些vm operation,因為我的Java線上還在運行,當 native code執行自己的東西的時候,是不會去碰到那些Java內部的那些hip hop object的那些東西,當想訪問那些object的時候,需要通過那些JMI的接口,當調用接口的時候,這個時候JVM就會來檢查這時候是不是正在做safepoint,如果正在做safepoint,就會把調用給阻塞,然後線程就會被停下來,等vm operation結束了以後再繼續執行下去。
所以雖然在Thread in native狀態你仍然在運行,但實際上不會造成造成危害,因為要訪問那種Java object或者訪問hip的時候,這裡的JMI接口會擋住。
8、Thread in java-interpreter
Thread in Java的解釋器模式,hotstpot中解釋器其實是通過一個叫dispatch table的一個數據結構來實現的,Dispatch table就是一個很大的 table,對於每個bite code,它對應的就是一小段的執行代碼,所以它執行的時候,是哪個bite code就執行 dispatch table中的哪一段代碼,然後在不停的跳轉。
在解釋器裡面,在hotstpot中,其實是維護了兩套dispatch table,一個就是normal table,這就是剛剛說對每個 bite code做解釋執行的代碼,另一個safept table,除了做正常的解釋執行之外,對每個bite code執行之前會加入一小段代碼來檢測,Jvm是不是發起了 safepoint的請求,如果發起safepoint的請求,就可以把自己給停下來。
通過這樣一個方式來safepoint的check的,正常的話, Java執行的都是normal table裡的bite code,如果 vm Thread決定發起一次safepoint的請求的時候,hotstpot內部有個active table的指針,它會做一次切換,從normal table中切換到了safept table。
一個bite code執行完,會去取下一次bite code的執行代碼,因為這時候已經被切換到了safept table,會執行ssafept table中對應的代碼,然後就會檢查safepoint,然後再暫停。
所以基本上可以理解在解釋器模式中,在每一次的bite code的最後都會做一次檢查,但實際上它是通過一個 table的表的一個切換來做的,正常運行的話,其實並沒有做檢查,所以它的性能並不會受影響。
9、Thread in java-jit
Jet最關注的是它的性能,在jet生成的code中,如何來檢查safepoint,在hotstpot裡,在它啟動的時候,會先申請一個全局的polling page的這一個頁,是一個4k大小的頁,然後在jit生成的代碼中,在某些特定的一個點,它會生成一兩條指令,直接去訪問頁,就去讀一下頁裡面這個內容是不是可讀,特定點大概有兩個,第一個是在jit code的返回的時候,在return的地方會去檢查一次;另一個是循環,如果代碼裡面有循環,它會在循環的 loop的back edge中,他\也會去檢查一次,只在這兩個點上去做檢查,一方面是確保他\檢查儘可能的少,另一方面要確保它的jit能夠及時的響應 safepoint的請求,本身只是讀一下,並沒有做任何的動作,這裡如何把自己給停下來,就是 vm Thread開始要觸發sfepoint的時候,會做一個動作,會把全局的pulling page把他的權限給改了,會用n protect類似於的API把權限設成不可訪問。
這樣如果讀取polling page的這條指令就會觸發一次SIGSEGV的異常,但 hotstpot本身在 signal handle裡面,會對這種SIGSEGV做進行一些特殊處理,它會捕獲住這種異常,會看觸發異常的地址,是不是polling page ,然後如果是個polling page的,就知道是jit裡面觸發的 safepoint,所以這裡並不是一個真正的異常,而是一次safepoint的請求。
後續的操作,會把 Java線程給暫停,然後把自己的狀態標誌為已經進入了 safepoint。
如下圖所示這段jit深層的代碼,裡面有一個 Loop的polling,又有一個 return的polling,可以看這兩條test的指令,用紅框標出來的,最上面的是一個 polling是一個在back edge中他用來做polling的,其實只是做一次test,把 polling地址放到了20寄存器中,然後就去讀一下test一下,後續對這個其實根本沒有任何操作, Test的結果對他來說沒有任何作用,就是為了去讀一次,能讀這個代碼就可以繼續往下執行。
下面的一條test,旁邊的標註是poll return,緊接著下面就是一個return的指令,所以這一條指令就是在return之前,也會去做一次polling,來判斷下是不是有人在發起了 safepoint的請求。
這就是在jit code中,大概會在這樣的兩個地方去做 polling,第二個test,如果看上一條,可能會看到20的地址其實是從二十五中讀取了一個偏移量過來,25在現在X86的hotstpot,主要是用來做一個thread,所以它其實是從thread中去讀了一個。
這裡說明一下,牽涉到新的一個 jdk10引入了一個技術,引入了一個叫thread local handkerchief,因為上述的 polling page是global的,實際上把 global的page把它作為這個地址記下來,然後每次polling的時候就直接去訪問這個地址,這就是一個常量,根本沒有任何動作不需要去到thread上去讀。
10、Global polling vs Thread Local handshake
在jdk10它這裡引入了一個叫thread the local的hand shake,這是一個新的協議品,主要的一個目的是要能夠對一個特定的thread來觸發safepoint,前面講過觸發safepoint以後是會讓所有的線程都停下來,但對某些操作,也許只是對一個線程來做動作的話,做一個把整個 Java線程全部停下來的操作,是一個比較比較浪費的一個行為。
所以希望就是說能夠用 thread local的機制,只對一個特定的thread來把它給暫停,在11裡面,都是用thread local,這時候他取polling page的時候,都是從通過自己的thread裡面去讀一個polling page的地址。
實際上怎麼做到thread local,其實上述的polling page中,做了兩個頁,一個就是好的每次都能讀,另一個是壞的,讀就肯定會失敗,good page和bad page這樣兩個頁,所以如果要對某個線程進行暫停的話,進入safepoint的話,其實就是把線程上的page的地址改寫一下,改成壞頁,這樣 thread就會觸發到異常來進入safepoint,這裡有一個開關,叫User ThreadLocal Handshakes,它現在默認是打開的,基本上默認都會去走thread local的 safepoint,如果還是想用global pulling,可以把它關掉。
實際上用到thread local,用特定的線程來進入safepoint的這種win其實也沒有多少,主要是現在的cgc大概會用到它。
Jit因為比較關注性能,如果那種loop在一個循環裡面,每個loop的回編中都要去做一個 polling,雖然只是一兩條指令,但如果是在一個大循環裡面,加起來的性能其實還是會有影響的,所以hotstpot為了提高它的性能,可以把counted loop的polling給去掉,counted loop就是一般看到的for loop,可以認為是那種for的循環,因為這種循環中會有一個循環變量,循環變量有初始值,有它的邊界,有它的布長,基本上都是固定的,在hotstpot裡面,就會認為這種循環叫counted loop,在counted loop裡面hotstpot可以做一個優化,把這種 polling的指令去掉,來提高它的性能,但這樣會造成它的一個trade off,如果你的counted loop比較大,這樣進safepoint的時間就會就會被推遲了。
因為在整個循環中都不會去檢查polling,都不會去檢查safepoint,要等這個循環執行完一直到最後退出的時候,才會檢查,造成的一個可能負面影響,就是說對進safepoint的時間它會延遲掉。
像G1/ZGC一些新的GC,這些機器更關注的是說暫停的時間,為了要把暫停時間給減少,所以這些GC的時候,又會默認把 counted loop中的pulling給生成出來。
總的開關,就是UseCountedLoopSafepoints ,打開就會生成,關掉就不生成這些polling。
11、監控safepoint
在日常的維護中,一般來說希望能知道safepoint究竟造成了一些行為是怎樣的,這裡提供的一些選項,像JDK8,主要是提供了,能夠打印safepoint的統計信息,能夠知道它大概發生了多少次,總的暫停時間,可以計算一下它的平均時間等。
但在JDK11中,已經把這一個選項基本上已經是廢棄了,因為在JDK11中,已經用了一個新的一套Log的機制,這套Log機制中對safepoint就可以用這個命令 logsafepoint=debug打開這個開關,會打印出很多的跟safepoint的詳細信息,如進入safepoint的花了多少時間,出來大概多少時間,總的時間是多少,這些詳細的這些信息都能夠在用 log來記,所以在JDK11中,其實是比較推薦用這種方式來看safepoint的這些數據。