Java虛擬機JVM之垃圾回收策略與算法

本章節主要介紹垃圾回收策略與算法

一、概述

二、判定對象是否存活

1、引用計數法

2、可達性分析法(主流)

三、引用類型

1、強引用(Strong Reference)

2、軟引用(Soft Reference)

3、弱引用(Weak Reference)

4、虛引用(Phantom Reference)

四、宣告對象死亡的兩次標記過程

1、第一次標記並進行一次篩選。

2、 第二次標記

五、方法區的垃圾回收

六、垃圾回收算法

1、標記-清除算法

2、複製算法(新生代)

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

4、分代收集算法


一、概述

程序計數器、虛擬機棧、本地方發棧這三個區域都是線程獨有的,隨線程而生,也隨線程而滅;棧幀隨着方法的開始而入棧,隨着方法的結束而出棧。這幾個區域的內存分配和回收都具備確認性,在這幾個區域內不需要過多考慮回收的問題,因爲方法結束或者線程結束時,內存自然就跟隨着回收了。

而對於 Java 堆和方法區,我們只有在程序運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的,垃圾收集器所關注的正是這部分內存。

哪些內存需要進行回收:Java 堆和方法區。

 

二、判定對象是否存活

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

1、引用計數法

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

引用計數算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。但是主流的 Java 虛擬機裏沒有選用引用計數算法來管理內存,主要是因爲它很難解決對象之間循環引用的問題。

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

 引用計數法的特點:

  •   需要單獨的字段存儲計數器,增加了存儲空間的開銷;
  •   每次賦值都需要更新計數器,增加了時間開銷;
  •   垃圾對象便於辨識,只要計數器爲0,就可作爲垃圾回收;
  •   及時回收垃圾,沒有延遲性;
  •   不能解決循環引用的問題;

2、可達性分析法(主流)

在Java中,是通過可達性分析(Reachability Analysis)來判定對象是否存活的。該算法的基本思路就是通過一些被稱爲引用鏈(GC Roots)的對象作爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。

GC Roots 是指:

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

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

 

三、引用類型

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

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

1、強引用(Strong Reference)

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

2、軟引用(Soft Reference)

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

import java.lang.ref.SoftReference;
 
public class Main {
    public static void main(String[] args) {
         
        SoftReference<String> sr = new SoftReference<String>(new String("hello"));
        System.out.println(sr.get());
    }
}

3、弱引用(Weak Reference)

弱引用的強度比軟引用更弱一些;被弱引用引用的對象只能生存到下一次垃圾收集前,一旦發生垃圾收集,被弱引用所引用的對象就會被清掉

import java.lang.ref.WeakReference;
 
public class Main {
    public static void main(String[] args) {
     
        WeakReference<String> sr = new WeakReference<String>(new String("hello"));
         
        System.out.println(sr.get());
        System.gc();                //通知JVM的gc進行垃圾回收
        System.out.println(sr.get());
    }
}

注意:在使用軟引用和弱引用的時候,我們可以顯示地通過System.gc()來通知JVM進行垃圾回收,但是要注意的是,雖然發出了通知,JVM不一定會立刻執行,也就是說這句是無法確保此時JVM一定會進行垃圾回收的。 

 

4、虛引用(Phantom Reference)

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

要注意的是,虛引用必須和引用隊列關聯使用,當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會把這個虛引用加入到與之 關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。如果程序發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
 
public class Main {
    public static void main(String[] args) {
        ReferenceQueue<String> queue = new ReferenceQueue<String>();
        PhantomReference<String> pr = new PhantomReference<String>(new String("hello"), queue);
        System.out.println(pr.get());
    }
}

注意:虛引用中有一個構造函數,可以看出,其必須和一個引用隊列一起存在。get()方法永遠返回null,因爲虛引用永遠不可達

 

四、宣告對象死亡的兩次標記過程

即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。標記的前提是對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈。
 

1、第一次標記並進行一次篩選。

篩選的條件是此對象是否有必要執行finalize()方法;當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”,對象被回收。
 

2、 第二次標記

如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲:F-Queue的隊列之中,並在稍後由一條虛擬機自動建立的、低優先級的Finalizer線程去執行。這裏所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),將很可能會導致F-Queue隊列中的其他對象永久處於等待狀態,甚至導致整個內存回收系統崩潰。finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。

注意:

  • 任何一個對象的 finalize() 方法只會被系統自動調用一次,如果對象面臨下一次回收,它的 finalize() 方法不會被再次執行,想繼續在 finalize() 中自救就失效了
  • finalize() 方法能做的,try-finally 都能做,所以忘了這個方法吧!

 

五、方法區的垃圾回收

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

  • 廢棄常量:例如一個“abc”字符串常量存在常量池中,但沒有任何String類型對象引用常量池中的這個常量,也沒有其它地方引用了這個常量,如果必要的話,這個常量也會被系統清理出常量池;常量池中的其他類(接口)、方法、字段的符號引用也與此類似
  • 無用的類:同時滿足以下 3 個條件的類。
    • 該類的所有實例已被回收,Java 堆中不存在該類的任何實例;
    • 加載該類的 Classloader 已被回收;
    • 該類的 Class 對象沒有被任何地方引用,即無法在任何地方通過反射訪問該類的方法。

注意:

  • 虛擬機是可以對滿足上述3個條件的類進行回收,僅僅是“可以”,並不是和對象一樣,不使用了就必然回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制
  •  在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

六、垃圾回收算法

1、標記-清除算法

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

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

不足

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

 

標記-清除算法

2、複製算法(新生代)

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

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

 

爲了解決空間利用率問題,可以將內存分爲三塊: 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 區。

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

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

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

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

標記整理算法

 

4、分代收集算法

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

  • 新生代: GC 過後只有少量對象存活 —— 複製算法
  • 老年代: GC 過後對象存活率高 —— 標記 - 整理算法

 

 

 

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