深入理解Java虛擬機(三)——垃圾回收策略

所謂垃圾收集器的作用就是回收內存空間中不需要了的內容,需要解決的問題是回收哪些數據,什麼時候回收,怎麼回收。
Java虛擬機的內存分爲五個部分:程序計數器、虛擬機棧、本地方法棧、堆和方法區。
其中程序計數器、虛擬機棧和本地方法棧是線程私有的,所以對於何時回收這三部分內存只需要根據線程的生存週期就可以了。
而堆和方法區是線程共享的,其誕生和銷燬伴隨的虛擬機的啓動和停止,所以需要特定的算法來判斷內存是否可以被回收。

堆內存的回收

判斷那些對象需要回收

垃圾回收器在回收之前,需要判斷那些對象已經不會被使用,那些是需要被使用的。對於那些無效的對象,有兩種判定算法:

  • 引用技術法:在對象中添加一個引用技術器,每當對象被引用一次時,計數器就加一;當一個引用失效時,計數器減一;當計數器的值爲零時,對象是不可能再被使用的,就是無效的。
  • 可達性分析算法:通過一列稱爲「GC Roots」的根對象作爲起始節點,根據它們所包含的引用關係,進行向下搜索,對於搜索到的對象都是有效的,沒有關聯的對象就是無效的。
    GC Roots包括:
    1. 虛擬機棧中引用的對象(棧幀中的局部變量表中引用類型變量所引用的對象)
    2. 方法區中靜態屬性引用的對象
    3. 方法去中常量引用對象
    4. 本地方法棧中JNI(即通常所說的Native方法)引用的對象
    5. 虛擬機內部的引用(基本數據類型對象的Class對象,異常對象,系統類加載器)
    6. 所有被同步鎖(synchronized)持有的對象
    7. 反映Java虛擬機內部情況的回調、本地代碼緩存

比較:引用計數法雖然簡單,但是無法解決循環引用的問題,目前主流的語言採用可達性方法。

回收無效對象的過程

要真正宣告一個對象的死亡,至少需要進行兩次標記:當在可達性算法標記之後,發現對象沒有引用鏈,就被第一次標記,隨後進程篩選,如果對象的finalize方法沒有被重寫或者finalize方法被調用過了,就進行第二次標記,這樣對象就死了。
如果finalize方法沒被調用,虛擬機就會給對象一次重生機會:

  1. 將對象的finalize方法放進F-Queue隊列中;
  2. 執行隊列中的所有finalize方法,虛擬機會以較低的優先級執行這些方法,不會確保所有的方法都被執行了,如果執行的時候超時了,就把產生這個方法的對象直接幹掉。
  3. 如果執行的時候重新將對象與GC Roots產生關聯,則對象就被救活了,如果沒有,就被幹掉。

注意:
強烈不建議使用finalize()函數進行任何操作!如果需要釋放資源,請使用try-finally。
因爲finalize()不確定性大,開銷大,無法保證順利執行。

方法區的內存回收

由於方法區中存放的信息生命週期較長,每次回收的信息較少,回收的性價比較低,所以方法區就是堆的老年代。

方法區的垃圾收集主要分爲兩部分內容:

  • 廢棄的常量
  • 不在使用的類型

廢棄放量判斷

和清除對象類似,如果常量沒有被引用,則被清理出常量池。

判斷廢棄的類型

同時滿足三個條件:

  • 該類所有的實例被回收。
  • 加載該類的類加載器被回收。
  • 該類對應的Java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類。只要一個類被虛擬機加載進方法區,那麼在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區的時候創建,在方法區中該類被刪除時清除。

垃圾回收算法

當我們知道內存中哪些區域需要被回收,那麼就需要垃圾回收算法來清理這些數據。

分代收集理論

分代收集實質是一套符合大多數程序運行實際情況的經驗法則,建立在兩個分代假說之上:

  1. 弱分代假說:絕大多數對象都是朝生夕滅的。
  2. 強分代假說:熬過越多次垃圾收集過程的對象就越難以消亡。
  3. 跨代引用假說,跨代引用相對於同代引用來說僅佔極少數。

這兩個分代假說奠定了垃圾收集的一致的原則設計:收集器應該將Java堆分爲不同的區域,然後將對象依據其年齡,將其分配到不同的區域中進行存儲。

Java虛擬機至少把Java堆分爲新生代老年代兩個區域。新生代中,每次垃圾回收都會有大量的對象被死去,然後存活少量的對象,將會逐步晉升到老年代中。

新生代中的對象可能會被老年代中的所引用,爲了找出新生代中存活的對象,不但要從GC Roots開始掃描,而且還需要遍歷整個老年代來獲得引用節點,這樣爲了少量跨代區掃描整個老年代,性價比太低。所以,只需要在新生代中建立一個全局的數據結構記憶集,這個結構把老年代分爲若干塊,並標識出那些塊會存在跨代引用,在GC掃描的時候,掃描這一部分老年代就好了,這樣節約時間。

分代收集定義

  • 部分收集(Partical GC):指的是不對整個Java堆進行垃圾收集,其中又分爲:
    • 新生代收集(Minor )
    • 老年代收集(Major GC/Old GC)
    • 混合收集(Mixed GC)
  • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。

標記-清除算法

首先標記所有需要回收的對象,然後統一回收被標記的對象,清楚數據。

缺點:執行效率不穩定,標記和秦楚的效率都隨着對象數量的增長而降低,而且會產生內存碎片,無法存儲較大的對象。
在這裏插入圖片描述

複製算法

將內存分爲兩個大小相等的部分,每次使用其中一塊,如果這一塊使用完了,就將存活的對象複製到另一塊,然後將這一塊的數據清除。

優點:不會產生內存碎片。
缺點:浪費了一半的內存空間,而且每次要將可用數據複製一下,效率低。
在這裏插入圖片描述

現在絕大多數的虛擬機採用這一算法來回收新生代,因爲新生代存活的對象少,每次需要複製的數據就比較少。

**解決空間利用率低的問題:**重新佈局新生代,將其分爲一塊較大的Eden空間(伊甸園)和兩塊較小的Survivor1和Survivior2空間,HotSpot對其默認比例是8:1:1。分配內存時,只使用Eden和Survivor1,發生垃圾收集時,將Eden和Survivor1中任然存活的的對象複製到Survivor2上,然後直接清理原有的兩個空間,然後將Survivor2 與Survivor1的空間引用互換,繼續分配。

這樣做,只有10%的新生代空間被浪費。

當Survivor空間不足以容納一次Minor GC之後的存活對象,就需要依賴其他區域進行分配擔保。所謂分配擔保,就是當Survivor區域內存不夠,向老年代借一些空間從來存儲對象。

標記-整理算法

在垃圾回收之前,將需要回收的垃圾進行標記,然後在垃圾回收的時候,將沒有被標記的數據放到一端,最後清除另一端的數據。
在這裏插入圖片描述

這是一種老年代的垃圾收集算法。

標記-清除算法與標記-整理算法的本質差異在於前者是一種非移動式的回收算法,而後者是移動式的。移動和不移動都會有優缺點,移動會耗時,不移動造成內存碎片,降低吞吐量。移動則內存回收時會更復雜,不移動則內存分配時會更復雜。

關注吞吐量就使用標記整理算法,關注延遲就使用標記清除算法。

吞吐量是指對網絡、設備、端口、虛電路或其他設施,單位時間內成功地傳送數據的數量。

算法混合使用

讓虛擬機平時多數時間都採用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經大到影響對象分配時,再採用標記-整理算法收集一次,以獲得規整的內存空間。前面提到的基於標記-清除算法的CMS收集器面臨空間碎片過多時採用的就是這種處理辦法。

Java中引用的種類

  1. 強引用:平時直接用引用就是強引用,只要存在強引用,則對象不會被回收。
  2. 軟引用:只要內存足夠就不會被回收,當出現OOM,就會被回收。通過SoftRerefence實現。
  3. 弱引用:只要發生垃圾回收,就會被回收。通過WeakReference類實現。
  4. 虛引用:無法通過虛引用訪問到對象的任何屬性和方法,唯一的作用時檢測垃圾收集器時候進行回收。通過PhantomReference類實現。