深入理解java虛擬機2

垃圾收集


一 哪些內存需要回收

引用計數算法(Reference Counting)雖然佔用了一些額外的內存空間來進行計數,但它的原理簡單,判定效率也很高。但這個看似簡單的算法有很多例外情況要考慮,譬如單純的引用計數就很難解決對象之間相互循環引用的問題。

主流的可達性分析算法的基本思路就是通過一系列稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。

可作爲GC Roots的對象包括以下幾種:

在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。

在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。

在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用。

在本地方法棧中JNI(即通常所說的Native方法)引用的對象。

Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。

所有被同步鎖(synchronized關鍵字)持有的對象。

反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

引用分爲強引用(Strongly Re-ference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

·強引用是最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。

· SoftReference類來實現軟引用。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。

·WeakReference類來實現弱引用。被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。

·PhantomReference類來實現虛引用。它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的只是爲了能在這個對象被收集器回收時收到一個系統通知。

finalize()方法

即使在可達性分析算法中判定爲不可達的對象,也不是“非死不可”的,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那麼虛擬機將這兩種情況都視爲“沒有必要執行”。

回收方法區

方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的類型。

廢棄的常量

已經沒有任何字符串對象引用常量池中的字符串常量,且虛擬機中也沒有其他地方引用這個字面量。則可以回收。

不再使用的類型

·該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。

·加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。

·該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這裏說的僅僅是“被允許”,而並不是和對象一樣,沒有引用了就必然會回收。


二 垃圾收集算法

商業虛擬機的垃圾收集器,大多數都遵循了“分代收集”的理論進行設計,分代收集名爲理論,實質是一套符合大多數程序運行實際情況的經驗法則,它建立在兩個分代假說之上:

1)弱分代假說:絕大多數對象都是朝生夕滅的。

如果一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的對象,就能以較低代價回收到大量的空間。

2)強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡。

如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區域,這就同時兼顧了垃圾收集的時間開銷和內存的空間有效利用。

3)跨代引用假說:跨代引用相對於同代引用來說僅佔極少數。

存在互相引用關係的兩個對象,是應該傾向於同時生存或者同時消亡的。

依據這條假說,就不應再爲了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數據結構(該結構被稱爲“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊內存會存在跨代引用。此後當發生Minor GC時,只有包含了跨代引用的小塊內存裏的對象纔會被加入到GC Roots進行掃描。

標記-清除算法

分爲“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。

標記-複製算法

原始的複製算法是將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

新生代中的對象有98%熬不過第一輪收集。因此並不需要按照1∶1的比例來劃分新生代的內存空間。具體做法是把新生代分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配內存只使用Eden和其中一塊Survivor。發生垃圾蒐集時,將Eden和Survivor中仍然存活的對象一次性複製到另外一塊Survivor空間上,然後直接清理掉Eden和已用過的那塊Survivor空間。

HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1,沒有辦法保證每次回收都只有不多於10%的對象存活,因此還有一個充當罕見情況的“逃生門”的安全設計,當Survivor空間不足以容納一次Minor GC之後存活的對象時,就需要依賴其他內存區域(大多就是老年代)進行分配擔保(Handle Promotion)。

標記-整理算法

老年代一般不選用標記-複製算法。“標記-整理”(Mark-Compact)算法其中的標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存。

從垃圾收集的停頓時間來看,不移動對象停頓時間會更短,甚至可以不需要停頓,但是從整個程序的吞吐量來看,移動對象會更划算。

HotSpot虛擬機裏面關注吞吐量的Parallel Scavenge收集器是基於標記-整理算法的,而關注延遲的CMS收集器則是基於標記-清除算法的,這也從側面印證這點。


三 算法細節

根節點枚舉

迄今爲止,所有收集器在根節點枚舉這一步驟時都是必須暫停用戶線程的,毫無疑問根節點枚舉與整理內存碎片一樣會面臨相似的“Stop The World”的困擾。

現在可達性分析算法耗時最長的查找引用鏈的過程已經可以做到與用戶線程一起併發,但根節點枚舉始終還是必須在一個能保障一致性的快照中才得以進行。

根節點枚舉並不需要一個不漏地檢查完所有執行上下文和全局的引用位置,而是使用一組稱爲OopMap的數據結構來直接得到哪些地方存放着對象引用的,從而快速準確地完成GC Roots枚舉。

安全點

HotSpot沒有爲每條指令都生成OopMap,只是在“特定的位置”記錄了這些信息,這些位置被稱爲安全點(Safepoint)。

有了安全點的設定,也就決定了用戶程序執行時並非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點後才能夠暫停。

安全點位置的選取基本上是以“是否具有讓程序長時間執行的特徵”爲標準進行選定的;

垃圾收集發生時,如何讓所有線程都跑到最近的安全點,然後停頓下來?

——主動式中斷的思想是當垃圾收集需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌位,各個線程執行過程時會不停地主動去輪詢這個標誌,一旦發現中斷標誌爲真時就自己在最近的安全點上主動中斷掛起。

HotSpot使用內存保護陷阱的方式,把輪詢操作精簡至只有一條彙編指令的程度,足夠高效。

安全區域

使用安全點來解決如何停頓用戶線程,讓虛擬機進入垃圾回收狀態,其實有一個bug。

安全點機制保證了線程執行時,可主動進入垃圾收集過程的安全點。但是,線程“不執行”的時候呢?

典型的場景便是用戶線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應虛擬機的中斷請求,不能再走到安全的地方去中斷掛起自己,虛擬機也顯然不可能持續等待線程重新被激活分配處理器時間。

安全區域是指能夠確保在某一段代碼片段之中,引用關係不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。可以把安全區域看作被擴展拉伸了的安全點

當用戶線程執行到安全區域裏面的代碼時,首先會標識自己已經進入了安全區域,當這段時間裏虛擬機要發起垃圾收集時就不必去管這些已聲明自己在安全區域內的線程了。

當線程要離開安全區域時,它要檢查虛擬機是否已經完成了根節點枚舉(或者垃圾收集過程中其他需要暫停用戶線程的階段),如果完成了,那線程就當作沒事發生過,繼續執行;否則它就必須一直等待,直到收到可以離開安全區域的信號爲止。

記憶集與卡表

記憶集是一種用於記錄從非收集區域指向收集區域的指針集合的抽象數據結構

用“卡表”(Card Table)的方式去實現記憶集,是目前最常用的一種記憶集實現形式

(之所以叫卡表,是因爲卡精度的概念:每個記錄精確到一塊內存區域,該區域內有對象含有跨代指針。)

卡表最簡單的形式可以是一個字節數組,而HotSpot虛擬機確實也是這樣做的。

字節數組的每一個元素都對應着其標識的內存區域中一塊特定大小的內存塊,這個內存塊被稱作“卡頁”(Card Page)。HotSpot中使用的卡頁大小是512字節。那如果卡表標識內存區域的起始地址是0x0000的話,數組的第0、1、2號元素,分別對應了地址範圍爲0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡頁內存塊。

一個卡頁的內存中通常包含不止一個對象,只要卡頁內有至少一個對象的字段存在着跨代指針,那就將對應卡表的數組元素的值標識爲1,稱爲這個元素變髒(Dirty),沒有則標識爲0。在垃圾收集發生時,只要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁內存塊中包含跨代指針,把它們加入GC Roots中一併掃描。

寫屏障

在HotSpot虛擬機裏是通過寫屏障(Write Barrier)技術維護卡表狀態的。

寫屏障可以看作在虛擬機層面對“引用類型字段賦值”這個動作的AOP切面,在引用對象賦值時會產生一個環形(Around)通知,用來更新卡表。

併發的可達性分析

先了解下三色標記的概念:

白色:表示對象尚未被垃圾收集器訪問過。顯然在可達性分析剛剛開始的階段,所有的對象都是白色的,若在分析結束的階段,仍然是白色的對象,即代表不可達。

·黑色:表示對象已經被垃圾收集器訪問過,且這個對象的所有引用都已經掃描過。黑色的對象代表已經掃描過,它是安全存活的,如果有其他對象引用指向了黑色對象,無須重新掃描一遍。黑色對象不可能直接(不經過灰色對象)指向某個白色對象。

·灰色:表示對象已經被垃圾收集器訪問過,但這個對象上至少存在一個引用還沒有被掃描過。

併發的可達性分析過程可能會出現“對象消失”問題。從三色標記的角度來理解它,即原本應該是黑色的對象被誤標爲白色,當且僅當以下兩個條件同時滿足時會產生“對象消失”的問題: 

·賦值器插入了一條或多條從黑色對象到白色對象的新引用;

·賦值器刪除了全部從灰色對象到該白色對象的直接或間接引用。

因此,要解決併發掃描時的對象消失問題,只需破壞這兩個條件的任意一個。產生了兩種解決方案:增量更新和原始快照,分別對應上面的兩個條件。

當發生上面的任一條件時,虛擬機就通過寫屏障將這個新插入的引用記錄下來,等併發掃描結束之後,再將這些記錄過的引用關係中的黑色或灰色對象爲根,重新掃描一次。

(CMS是基於增量更新來做併發標記的,G1、Shenandoah則是用原始快照來實現。)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章