本文已收錄至Github,推薦閱讀 ?? Java隨想錄
微信公眾號:Java隨想錄
CSDN: 碼農BookSea
目錄不知道自己的無知,乃是雙倍的無知,——柏拉圖
- 跨代參考問題
- 記憶集
- 卡表
- 寫屏障
- 寫屏障的偽共享問題
跨代參考問題
跨代參考是指新生代中存在對老年代物件的參考,或者老年代中存在對新生代的參考,
假如要現在進行一次只局限于新生代區域內的收集(Minor GC),但新生代中的物件是完全有可能被老年代所參考的,為了找出該區域中的存活物件,不得不在固定的 GC Roots 之外,再額外遍歷整個老年代中所有物件來確保可達性分析結果的正確性,反過來也是一樣,無疑會為記憶體回收帶來很大的性能負擔,
別慌,JVM的設計者已經考慮了這個場景,并想到了解決辦法,那就是使用一種叫做:記憶集(Remembered Set)的資料結構,
記憶集
記憶集位于新生代中,用以避免把整個老年代加進GC Roots掃描范圍,
記憶集的作用和我們之前講的OopMap很相似,維護了類似一種映射表的關系,避免了全域掃描,本質是用空間換時間,
記憶集是一種用于記錄從非收集區域指向收集區域的指標集合的抽象資料結構,注意這里的說辭:抽象,意思就是說記憶集是一種邏輯上的概念,并沒有規定具體的實作,類似方法區,下文我們會說到卡表,可以把記憶集和卡表的關系理解為Map跟HashMap,
卡表
卡表可以理解為是記憶集的具體實作,英文叫:Card Table
垃圾收集器只需要通過記憶集判斷出某一塊非收集區域是否存在有指向了收集區域的指標就可以了,并不需要了解這些跨代指標的全部細節,那設計者在實作記憶集的時候,便可以選擇更為粗獷的記錄粒度來節省記憶集的存盤和維護成本,下面列舉了一些可供選擇(當然也可以選擇這個范圍以外的)的記錄精度:
其中,第三種“卡精度”所指的就是“卡表”的方式去實作記憶集 ,這也是目前最常用的一種記憶集實作形式,HotSpot采用的就是卡表,
在HotSpot虛擬機里面,卡表采用的是位元組陣列的形式,以下這行代碼是HotSpot默認的卡表標記邏輯 :
CARD_TABLE [this address >> 9] = 0;
位元組陣列CARD_TABLE的每一個元素都對應著其標識的記憶體區域中一塊特定大小的記憶體塊,這個記憶體塊被稱作“卡頁”(Card Page),一般來說,卡頁大小都是以2的N次冪的位元組數,通過上面代碼可以看出HotSpot中使用的卡頁是2的9次冪,即512位元組,那如果卡表標識記憶體區域的起始地址是0x0000的話,陣列CARD_TABLE的第0、1、2號元素,分別對應了地址范圍為0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡頁記憶體塊 ,如圖所示:
一個卡頁的記憶體中通常包含不止一個物件,只要卡頁內有一個(或更多)物件的欄位存在著跨代指標,那就將對應卡表的陣列元素的值標識為1,稱為這個元素變臟(Dirty),沒有則標識為0,在垃圾收集發生時,只要篩選出卡表中變臟的元素,就能輕易得出哪些卡頁記憶體塊中包含跨代指標,把它們加入GC Roots中一并掃描,
簡單來說,就是卡頁的位元組陣列只有0和1兩種狀態,1表示哪些記憶體區域存在跨代指標,那么只要把1的加入GC Roots中一并掃描,就能知道哪些進行跨代參考了,這樣就不用挨個去掃描了,
OK,我們還剩下一個問題,這個問題OopMap也遇到過,卡表元素如何維護?何時變臟、誰來把它們變臟等,
HotSpot解決的辦法是使用寫屏障,
寫屏障
先來解決何時變臟的問題,這個問題很簡單,即其他分代區域中物件參考了本區域物件時,其對應的卡表元素就應該變臟,變臟時間點原則上應該發生在參考型別欄位賦值的那一刻,
但問題是如何變臟,即如何在物件賦值的那一刻去更新維護卡表,在HotSpot虛擬機里是通過寫屏障(Write Barrier)解決的,
注意:這里提到的 寫屏障 和 volatile 的寫屏障不是一回事,
寫屏障可以看作在虛擬機層面對“參考型別欄位賦值”這個動作的AOP切面,在參考物件賦值時會產生一個環形(Around)通知,用過Spring的弟兄們對AOP肯定不陌生,
在賦值前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在賦值后的則叫作寫后屏障(Post-Write Barrier),HotSpot虛擬機的許多收集器中都有使用到寫屏障,但直至G1收集器出現之前,其他收集器都只用到了寫后屏障,
應用寫屏障后,虛擬機就會為所有賦值操作生成相應的指令,一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對新生代物件的參考,每次只要對參考進行更新,就會產生額外的開銷,不過這個開銷與Minor GC時掃描整個老年代的代價相比還是低得多的,
寫屏障的偽共享問題
卡表在高并發場景下還面臨著“偽共享”(False Sharing)問題,偽共享是處理并發底層細節時一種經常需要考慮的問題,號稱并發的隱形殺手,現代中央處理器的快取系統中是以快取行(Cache Line)為單位存盤的,當多執行緒修改互相獨立的變數時,如果這些變數恰好共享同一個快取行,就會彼此影響(寫回、無效化或者同步)而導致性能降低,這就是偽共享問題,
為了避免偽共享問題,一種簡單的解決方案是不采用無條件的寫屏障,而是先檢查卡表標記,只有當該卡表元素未被標記過時才將其標記為變臟,即將卡表更新的邏輯變為以下代碼所示:
相當于說就是多了一個if判斷條件,
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
在JDK 7之后,HotSpot虛擬機增加了一個新的引數-XX:+UseCondCardMark(默認是關閉的),用來決定是否開啟卡表更新的條件判斷,開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有性能損耗,是否打開要根據應用實際運行情況來進行測驗權衡,
如果本篇博客有任何錯誤和建議,歡迎給我留言指正,文章持續更新,可以關注公眾號第一時間閱讀,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/542586.html
標籤:其他
