您好,登錄后才能下訂單哦!
本篇內容介紹了“如何實現GC ”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
?這部分的內容,筆者點到為止,覺得看的不爽的歡迎進群一起討論。因為不確定的東西我不能寫出來誤導別人,要做一個講筆德的作者。
?
我站在周老師的肩上高歌 ”HotSpot 是這么實現的垃圾收集器!“
通過上一篇的內容我們知道了一些可以固定作為 GC Roots 的內容,他們包括靜態變量、常量、方法運行時上下文。我們也知道了可達性分析算法 (這里如果不清楚的請參考筆者前兩篇文章內容,>" data-itemshowtype="0" tab="innerlink" data-linktype="2">這里放個傳送門>>) 。不過運行時這么多引用,全部都掃描一遍這啥虛擬機也受不了啊,GC 2秒鐘,掃描8小時啊。
所以就有了第一階段的根結點枚舉,這一步就是直接掃描與 GC Roots 直接相關的那部分內容。這一步的操作需要 “Stop The World”(Stop The World 就是用來形容在安全點用戶線程暫停的這種狀態的一個叫法,關于安全點接下來就會提到)。
可達性分析時,并不會全部的挨個掃描執行上下文和全局引用。在 HotSpot 中,有一個叫做 OopMap 的數據結構,專門存放著引用信息,這個普通對象指針是在類加載和即時編譯時分別將全局引用和執行上下文「特定」的相關位置記錄下來的。(這地方與后面的內容有關,記一下)
?OopMap( Ordinary Object Pointer) 點到為止,這部分內容可以根據代碼的編譯結果看到,感興趣的可以研究研究。圖片來自《深入理解 Java 虛擬機》3.4.1 代碼清單 3-3
?
通過上面我們知道 GC 要做的事是通過 OopMap 找出來那些被引用的對象,而這個 OopMap 里面存了兩種數據,一部分是全局引用,這好說,類加載的時候懟上,不會變了。那執行上下文怎么辦?那一個個方法的調用,一個個棧幀,棧幀里又那么多變量 (這部分內容在前面已經學過了,如果不清楚可以回到前面文章復習,>" data-itemshowtype="0" tab="innerlink" data-linktype="2">再次召喚傳送門>>) 。如果把全部的字節碼指令全部都存下來那不瘋了?所以 hotspot 沒瘋,它只存了一些特定的位置把這個信息記到 OopMap 中。在程序執行過程中會有多個這樣的特定位置,這些特定的位置就被稱為 「安全點」 。
有了安全點我們就應該知道了,GC 不是任何時候都能做的。必須要等到程序到達安全點之后才能做。為啥應該不難理解吧,兩個安全點之間如果你執行了 GC ,是不是會導致一部分執行上下文相關的引用你不知道,因為 OopMap 里面只存了最近一個安全點內的指令內容。
明白了這個必須等到安全點才能 GC 之后,又有新的問題了,( GC 做一次真是太難了)你說這個安全點,你放多少個合適,間隔又要多少才合理,放遠了吧,半天半天不能做一次 GC ,放近了吧倒是隨時想做就能做,但是你要知道這個安全點也是一條指令啊,那插入那么多額外的指令到程序中你覺得合適嗎?而且這玩應也要存儲啊不是 OopMap 了解一下。于是 hotspot 的開發者就研究。最后來有了一個比較銀杏的解決辦法。
因為一般指令執行的時間很短,所以這個解決辦法就是,在一些長時間執行的部分給它懟一個安全點,防止程序長時間執行我沒辦法 GC ,根據長時間執行的特征,有些地方就顯而易見的被選出來祭天了,它們是 方法調用
、循環回邊處
、異常跳轉
現在我們知道了 GC 需要通過 OopMap 找到 GC Roots 中的相關引用,又知道了要在安全點的時候暫停的時候開始找這些引用,但又有問題了,我知道 GC 要在線程執行到安全點的時候暫停,可怎么才能讓每一個線程到達最近的安全點上,并且暫停呢?
兩種辦法,虛擬機強行等你到安全點,還有一種就全憑自覺。
什么叫虛擬機強行等你到安全點呢,他還有個名字叫 「Preemptive Suspension」,就是 先發制人
(搶先式中斷) 。虛擬機直接中斷用戶線程,然后看你到沒到安全點,沒到繼續跑,然后在中斷。再看再跑再看再跑,直到全部線程都到達安全點,over 任務完成。
相比虛擬機懟我到安全點,我還不如自覺點 **Voluntary Suspension ** 主動式中斷
。虛擬機會發出一個安全點集合信號,所有線程輪詢這個集合信號,一旦信號為真時,當前線程會在最近的一個安全點到達時掛起。
人生苦短,我選自覺。現在大部分虛擬機都是選的自覺方式來到達安全點。畢竟先發制人太不講武德了。
?點到為止內容,就是線程的這個輪詢操作的實現。因為需要頻繁執行,且高效。HotSpot 只使用了一條匯編指令實現了這個操作。
?
test %eax,0x160100
當需要暫停用戶線程時, 虛擬機把0x160100的內存頁設置為不可讀, 那線程執行到test指令時就會產生一個自陷異常信號, 然后在預先注冊的異常處理器中掛起線程實現等待。
這部分可以算是安全點的擴展,因為程序執行過程中,不能保證線程全部都在運行狀態,或等待或阻塞等等,所以就有了安全區域的概念,這部分區域內容標志著在這個區域中,對象的引用關系不會發生改變。不會影響 GC 正常進行,當用戶線程執行到安全區域后會標志自己現在在安全區域, GC 不要管我,等到用戶線程從安全區域出來的時候要和 GC 打招呼,“GC 你完事了嗎?我要出來了” 如果這個時候沒有 GC 動作,那你就可以出來了,如果這個時候在 根結點枚舉 階段,或在收集過程需要用戶線程暫停的階段,那么用戶線程就需要等待,知道 GC 結束才能從安全區域出來。
上面的 GC 過程,在只有新生代內存被使用,老年代沒有使用的時候還是沒問題的,但是一旦出現之前文章提到過的跨代引用問題,就需要考慮了,跨代引用是指老年代中存在引用新生代對象的指針。為了解決對象跨代引用所帶來的問題,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數據結構,一種用于記錄從非收集區域指向收集區域的指針集合的抽象數據結構。這個在后面不光用在了這種只有新生代和老年代的收集器中,后面的區域收集器也會用到。
?區域收集器指的是 G1 ,ZGC 還有 Shenandoah收集器這種。
?
有了記憶集的概念之后,就考慮怎么保存含有跨代引用的信息,可以將有跨代引用的對象全部保存下來,但是這樣做太占內存,而且維護起來也不方便。于是有一種較好的記錄方案,就是按區域劃分內存,將有跨代引用的那部分內存區域記錄下來,這種實現方式稱為 “卡表”。
HotSpot 將整個堆劃分為一個個大小為 512 字節的卡頁,維護成一個卡表,每個卡表的大小默認為 1 個字節用來存儲每張卡的一個標識位0或者1。這個標識位代表對應的卡「是否可能存有指向新生代對象的引用」。如果可能存在,那么這張卡就是 「臟卡」。在 GC 的時候,只需要篩選臟卡對應內存區域中的對象就好了,不需要掃描全部的對象。
?注意不要搞混記憶集與卡表的概念,一個是定義的數據結構,另一個是具體的實現方法。
?
知道了用卡表來解決跨代或跨內存區域的問題,當某個卡頁可能存在跨代引用時就會變臟,那這個變臟的過程是怎么樣的呢?又是怎么實現的呢?
正常情況下,卡表變臟的時機是當前區域中的對象中,引用了其他區域的對象,此時更新這張表為臟表。如果解釋執行,一條條執行下去可以,虛擬機可以根據變量賦值的指令來判斷,進行相應的操作,但是在即時編譯過程中,這個就需要一些對應的機器指令操作了。在HotSpot虛擬機里是通過寫屏障(Write Barrier)技術來維護卡表狀態的。「與 volatile 的重排序屏障指令不同!!!」
這個寫屏障的具體實現分為兩個,一個叫做寫前屏障,一個叫做寫后屏障。他們的操作類似 AOP ,他們可以在一個變量賦值操作前后做出一個通知。在 hotspot 中大多使用了寫后屏障。這樣就可以在變量賦值操作之后,將其對應的卡表更新為臟表。
寫屏障帶來了一個問題,這個問題是由 CPU 引起的,現在的 CPU 緩存中都是有一個個緩沖行保存的數據,在多核處理器的情況下,可能存在多個線程共享一個緩沖行的情況,比如一個緩沖行的大小是 32 kb,那么一張存有 64 張卡頁的卡表(64 * 512字節)就有可能在同一個緩沖行上面。為了解決多個線程同時更新同一個緩沖行浪費的性能開銷。hotspot 在更新卡表狀態時,加入了一個當前卡表是否為臟表的判斷,如果是臟表就不再進行更新操作。
?在JDK 7之后,HotSpot虛擬機增加了一個新的參數-XX:+UseCondCardMark,用來決定是否開啟卡表更新的條件判斷。開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有性能損耗,是否打開要根據應用實際運行情況來進行測試權衡。
?
上面已經對整個垃圾回收過程涉及的細節過了一遍,接下來就要看看其中的重頭戲,可達性分析算法了,也就是上面一直說的掃描掃描的內個。
我們知道可達性分析算法是需要暫停用戶線程才能夠使用,就是需要 Stop The World ,根結點枚舉這一步的暫停時間雖然很短,但是還是要暫停的,同時這個暫停的時候會隨著系統的對象的增長而增長,成正比關系。
可達性分析算法的描述目前都是采用三色標記來輔助理解的。
?希望這塊的內容能夠和之前的 finalize 方法聯系起來,還記得之前文章中我們提到的自己救自己一次的那個地方嗎,待會可以倒過去看一看,這可以幫助你加深這塊的理解,當然也只有我才會給你說這么細的提醒
?
?周老師《深入理解 Java 虛擬機》(第三版)3.4.6插圖
此例子中的圖片引用了Aleksey Shipilev在DEVOXX 2017上的主題演講:《Shenandoah GC Part I:The Garbage Collector That Could》。
?
上圖最后兩個情況說明了在并發階段的標記問題。因為并發標記是指 GC 的工作線程與用戶線程并發執行,所以就會出現一邊標記一邊改變對象引用的情況。
并發標記會出現兩類問題,一類是漏標,一類是誤標。漏標是指某個應該為白色的對象沒有被標記成白色,這種問題一般不會有太大影響。最多浪費一部分內存在下一次 GC 時將其再次標記回收。而另一類問題就是誤標。這兩個問題在上圖的最后兩個里面可以體現出來。
誤標的危害是很嚴重的,如果一個正在引用的對象,被誤標記成了白色。那么 GC 結束之后這個對象被清除,可能直接導致系統崩潰。
這個問題的出現原因有被證實過,當且僅當滿足以下兩點時才會出現誤標的情況
通過這兩個情況,我們也不難理解誤標的產生,因為黑色節點的規則是不會在掃描,而灰色則是會再進行掃描。所以對應的解決辦法也比較清晰,只需要不要讓以上兩個條件同時滿足即可。HotSpot 針對以上兩點分別使用了「增量更新」和「原始快照」兩種解決方案。
增量更新的意思是指,如果一個引用關系是從黑色節點指向白色節點,那么就需要在并發標記結束對這些個黑色節點作為根節點,重新進行掃描,即黑色節點發生新的引用關系后,其會變成灰色節點。(CMS 收集器中的重新標記使用的這種方案)
原始快照指的是,如果一個灰色節點刪除了指向白色節點的引用,那么需要將這個刪除的引用記錄下來,在并發標記結束對這個記錄的引用關系中灰色節點作為根結點重新掃描。無論這個對象是否刪除了,都會重新再掃描一次。(G1 的最終標記使用的這種方案)
“如何實現GC ”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。