垃圾收集策略與算法

在前面的博文當中我們已經介紹了java運行時內存區域的各個部分及特點,其中程序計數器、虛擬機棧、 本地方法棧三個區域隨着線程而生,隨着線程而滅;棧中的棧幀隨着方法的執行和退出,有條不紊的執行者入棧和出棧操作。每一個棧幀分配 多少內存基本上是在類結構確定下來的時候就 已知的,因此這幾個區域的內存分配和回收都具備確定性, 在這幾個區域 就不用過多的考慮回收問題,所以所謂的垃圾回收主要針對的區域是堆,這部分的內存分配是動態的。

判定對象是否存活

若一個對象不被任何對象或變量引用,那麼它就是無效對象,需要被回收。

引用計數法

在對象頭維護着一個 counter 計數器,對象被引用一次則計數器 +1;若引用失效則計數器 -1。當計數器爲 0 時,就認爲該對象無效了。

引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。也有一些比較著名應用但是主流案例,例如微軟公司的COM技術,使用ActionScript3的FlashPlayer、Python語言已經在遊戲腳本領域被廣泛應用的Squirrel都使用了引用計數法就行內存管理,但是,至少主流 Java 虛擬機裏沒有選用引用計數算法來管理內存,主要是因爲它很難解決對象之間循環引用的問題。

舉個栗子對象 objA 和 objB 都有字段 instance,令 objA.instance = objB 並且 objB.instance = objA,由於它們互相引用着對方,導致它們的引用計數都不爲 0,於是引用計數算法無法通知 GC 收集器回收它們。

可達性分析法

所有和 GC Roots 直接或間接關聯的對象都是有效對象,和 GC Roots 沒有關聯的對象就是無效對象。

GC Roots 是指:

  • Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧中引用的對象
  • 方法區中常量引用的對象
  • 方法區中類靜態屬性引用的對象

GC Roots 並不包括堆中對象所引用的對象,這樣就不會有循環引用的問題。

image

 

引用的種類

判定對象是否存活與“引用”有關。在 JDK 1.2 以前,Java 中的引用定義很傳統,一個對象只有被引用或者沒有被引用兩種狀態,我們希望能描述這一類對象:當內存空間還足夠時,則保留在內存中;如果內存空間在進行垃圾手收集後還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。

在 JDK 1.2 之後,Java 對引用的概念進行了擴充,將引用分爲了以下四種。不同的引用類型,主要體現的是對象不同的可達性狀態reachable和垃圾收集的影響。

強引用(Strong Reference)

類似 “Object obj = new Object()” 這類的引用,就是強引用,只要強引用存在,垃圾收集器永遠不會回收被引用的對象。但是,如果我們錯誤地保持了強引用,比如:賦值給了 static 變量,那麼對象在很長一段時間內不會被回收,會產生內存泄漏。

軟引用(Soft Reference)

軟引用是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現內存敏感的緩存,如果還有空閒內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
 

弱引用(Weak Reference)

弱引用的強度比軟引用更弱一些。當 JVM 進行垃圾回收時,無論內存是否充足,都會回收只被弱引用關聯的對象。

虛引用(Phantom Reference)

虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響。它僅僅是提供了一種確保對象被 finalize 以後,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制。

對象生存還是死亡

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。
如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個內存回收系統崩潰。finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。從代碼清單3-2中我們可以看到一個對象的finalize()被執行,但是它仍然可以存活。

回收方法區內存

方法區中存放生命週期較長的類信息、常量、靜態變量,每次垃圾收集只有少量的垃圾被清除。方法區中主要清除兩種垃圾:

  • 廢棄常量
  • 無用的類

判定廢棄常量

只要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除掉。比如,一個字符串 “bingo” 進入了常量池,但是當前系統沒有任何一個 String 對象引用常量池中的 “bingo” 常量,也沒有其它地方引用這個字面量,必要的話,”bingo”常量會被清理出常量池。

判定無用的類

判定一個類是否是“無用的類”,條件較爲苛刻。

  • 該類的所有對象都已經被清除
  • 加載該類的 ClassLoader 已經被回收
  • 該類的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

一個類被虛擬機加載進方法區,那麼在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區時創建,在方法區該類被刪除時清除。

垃圾收集算法

學會了如何判定無效對象、無用類、廢棄常量之後,剩餘工作就是回收這些垃圾。常見的垃圾收集算法有以下幾個:

標記-清除算法

標記的過程是:遍歷所有的 GC Roots,然後將所有 GC Roots 可達的對象標記爲存活的對象

清除的過程將遍歷堆中所有的對象,將沒有標記的對象全部清除掉。與此同時,清除那些被標記過的對象的標記,以便下次的垃圾回收。

這種方法有兩個不足

  • 效率問題:標記和清除兩個過程的效率都不高。
  • 空間問題:標記清除之後會產生大量不連續的內存碎片,碎片太多可能導致以後需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

複製算法(新生代)

爲了解決效率問題,“複製”收集算法出現了。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,需要進行垃圾收集時,就將存活者的對象複製到另一塊上面,然後將第一塊內存全部清除。這種算法有優有劣:

  • 優點:不會有內存碎片的問題。
  • 缺點:內存縮小爲原來的一半,浪費空間。

爲了解決空間利用率問題,可以將內存分爲三塊: Eden、From Survivor、To Survivor,比例是 8:1:1,每次使用 Eden 和其中一塊 Survivor。回收時,將 Eden 和 Survivor 中還存活的對象一次性複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛纔使用的 Survivor 空間。這樣只有 10% 的內存被浪費。

但是我們無法保證每次回收都只有不多於 10% 的對象存活,當 Survivor 空間不夠,需要依賴其他內存(指老年代)進行分配擔保。

分配擔保

爲對象分配內存空間時,如果 Eden+Survivor 中空閒區域無法裝下該對象,會觸發 MinorGC 進行垃圾收集。但如果 Minor GC 過後依然有超過 10% 的對象存活,這樣存活的對象直接通過分配擔保機制進入老年代,然後再將新對象存入 Eden 區。

標記-整理算法(老年代)

標記:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷 GC Roots,然後將存活的對象標記。

整理:移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收。因此,第二階段才稱爲整理階段。

這是一種老年代的垃圾收集算法。老年代的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,如果採用複製算法,每次需要複製大量存活的對象,效率很低。

分代收集算法

根據對象存活週期的不同,將內存劃分爲幾塊。一般是把 Java 堆分爲新生代和老年代,針對各個年代的特點採用最適當的收集算法。

  • 新生代:複製算法
  • 老年代:標記-清除算法、標記-整理算法

 

發佈了364 篇原創文章 · 獲贊 389 · 訪問量 141萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章