- 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的這些資料。