關於OopMap、SafePoint(安全點)以及安全區域

1.OopMapjava

以前咱們提到,在正式的GC以前老是須要進行可達性分析來查找內存中全部存活的對象,以便GC可以正確的回收已經死亡的對象。那麼對於一個十分複雜的系統,每次GC的時候都要遍歷全部的引用確定是不現實的。由於在可達性分析的時候,須要進行Stop The World,程序中的線程須要中止來配合可達性分析。就好像是你女友在打掃衛生的時候(什麼,你尚未女友?這還能難道程序員了?new 一個啊!),確定不會讓你走來走去的。因此,你確定在心裏裏也但願你女友打掃衛生快一點,由於你的膀胱已經快要爆炸了。對於程序來講有也同樣,也但願GC的時候快一點,以便讓程序高效地完成工做。程序員

因此,每次直接遍歷整個引用鏈確定是不現實的。 爲了應對這種尷尬的問題,最先有保守式GC和後來的準確式GC。這裏準確式GC就會提到一個OopMap,用來保存類型的映射表。算法

  1. 保守式GC

在進行GC的時候,會從一些已知的位置(GC Roots)開始掃描內存,掃描到一個數字就判斷他是否是多是指向GC堆中的一個指針(這裏會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(一般分配空間的時候會有對齊要求,假如說是4字節對齊,那麼不能被4整除的數字就確定不是指針),之類的。)。而後一直遞歸的掃描下去,最後完成可達性分析。這種模糊的判斷方法由於沒法準確判斷一個位置上是不是真的指向GC堆中的指針,因此被命名爲保守式GC。這種可達性分析的方式由於不須要準確的判斷出一個指針,因此效率快,可是也正由於這種特色,他存在下面兩個明顯的缺點:安全

  • 由於是模糊的檢查,因此對於一些已經死掉的對象,極可能會被誤認爲仍有地方引用他們,GC也就天然不會回收他們,從而引發了無用的內存佔用,就是典型的佔着茅坑不拉屎,形成資源浪費。
  • 因爲不知道疑似指針是否真的是指針,因此它們的值都不能改寫;移動對象就意味着要修正指針。換言之,對象就不可移動了。有一種辦法能夠在使用保守式GC的同時支持對象的移動,那就是增長一個間接層,不直接經過指針來實現引用,而是添加一層「句柄」(handle)在中間,全部引用先指到一個句柄表裏,再從句柄表找到實際對象。這樣,要移動對象的話,只要修改句柄表裏的內容便可。可是這樣的話引用的訪問速度就下降了。Sun JDK的Classic VM用過這種全handle的設計,但效果實在算不上好。

2.準確式GC性能

與保守式GC相對的就是準確式GC,何爲準確式GC?就是咱們準確的知道,某個位置上面是不是指針,對於java來講,就是知道對於某個位置上的數據是什麼類型的,這樣就能夠判斷出全部的位置上的數據是否是指向GC堆的引用,包括棧和寄存器裏的數據。線程

網上看了下說是實現這種要求的方法有好幾種,可是在java中實現的方式是:從我外部記錄下類型信息,存成映射表,在HotSpot中把這種映射表稱之爲OopMap,不一樣的虛擬機名稱可能不同。設計

實現這種功能,須要虛擬機的解釋器和JIT編譯器支持,由他們來生成OopMap。生成這樣的映射表通常有兩種方式:指針

  • 每次都遍歷原始的映射表,循環的一個個偏移量掃描過去;這種用法也叫「解釋式」; 
  • 爲每一個映射表生成一塊定製的掃描代碼(想像掃描映射表的循環被展開的樣子),之後每次要用映射表就直接執行生成的掃描代碼;這種用法也叫「編譯式」。

總而言之,GC開始的時候,就經過OopMap這樣的一個映射表知道,在對象內的什麼偏移量上是什麼類型的數據,並且特定的位置記錄下棧和寄存器中哪些位置是引用。對象

 

2.SafePoint(安全點)遞歸

上面講到了爲了快點進行可達性的分析,使用了一個引用類型的映射表,能夠快速的知道對象內或者棧和寄存器中哪些位置是引用了。

可是隨着而來的又有一個問題,就是在方法執行的過程當中, 可能會致使引用關係發生變化,那麼保存的OopMap就要隨着變化。若是每次引用關係發生了變化都要去修改OopMap的話,這又是一件成本很高的事情。因此這裏就引入了安全點的概念。

什麼是安全點?OopMap的做用是爲了在GC的時候,快速進行可達性分析,因此OopMap並不須要一發生改變就去更新這個映射表。只要這個更新在GC發生以前就能夠了。因此OopMap只須要在預先選定的一些位置上記錄變化的OopMap就好了。這些特定的點就是SafePoint(安全點)。由此也能夠知道,程序並非在全部的位置上均可以進行GC的,只有在達到這樣的安全點才能暫停下來進行GC。

既然安全點決定了GC的時機,那麼安全點的選擇就至爲重要了。安全點太少,會讓GC等待的時間太長,太多會浪費性能。因此安全點的選擇是以程序「是否具備讓程序長時間執行的特徵」爲標準的(這句話是從書上看來的,不知道做者本身能不能看明白這話啥意思,反正我是看不懂),因此咱們這裏瞭解一下結果就好了。通常會在以下幾個位置選擇安全點:

  1. 循環的末尾 
  2. 方法臨返回前 / 調用方法的call指令後 
  3. 可能拋異常的位置

還有一個須要考慮的問題就是,如何讓程序在要進行GC的時候都跑到最近的安全點上停頓下來。這裏有兩種方案:

  1. 搶斷式中斷

搶斷式中斷就是在GC的時候,讓全部的線程都中斷,若是這些線程中發現中斷地方不在安全點上的,就恢復線程,讓他們從新跑起來,直到跑到安全點上。(如今幾乎沒有虛擬機採用這種方式,緣由不詳)

  1. 主動式中斷

主動式中斷在GC的時候,不會主動去中斷線程,僅僅是設置一個標誌,當程序運行到安全點時就去輪訓該位置,發現該位置被設置爲真時就本身中斷掛起。因此輪訓標誌的地方是和安全點重合的,另外建立對象須要分配內存的地方也須要輪詢該位置。

 

3.安全區域

安全點的使用彷佛解決了OopMap計算的效率的問題,可是這裏還有一個問題。安全點須要程序本身跑過去,那麼對於那些已經停在路邊休息或者看風景的程序(好比那些處在Sleep或者Blocked狀態的線程),他們可能並不會在很短的時間內跑到安全點去。因此這裏爲了解決這個問題,又引入了安全區域的概念。

安全區域很好理解,就是在程序的一段代碼片斷中並不會致使引用關係發生變化,也就不用去更新OopMap表了,那麼在這段代碼區域內任何地方進行GC都是沒有問題的。這段區域就稱之爲安全區域。線程執行的過程當中,若是進入到安全區域內,就會標誌本身已經進行到安全區域了。那麼虛擬機要進行GC的時候,發現該線程已經運行到安全區域,就不會管該線程的死活了。因此,該線程在脫離安全區域的時候,要本身檢查系統是否已經完成了GC或者根節點枚舉(這個跟GC的算法有關係),若是完成了就繼續執行,若是未完成,它就必須等待收到能夠安全離開安全區域的Safe Region的信號爲止。

相關文章
相關標籤/搜索