0040-垃圾收集算法

1. 前言

垃圾回收分爲兩個階段,首先確認哪些對象是垃圾——標記階段,其次是垃圾確認以後的回收——回收階段

2. 標記算法

2.1 引用計數算法

1. 簡述

對於一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的計數器的值爲0,即表示對象A不可能再被使用,可進行回收。

2. 優點

實現簡單,垃圾對象便於辨識,判斷效率高,回收沒有延遲性

3. 缺點

  1. 每個對象需要一個引用計數器屬性,用於記錄對象被引用的情況,這個做法增加了存儲空間的開銷

  2. 每次賦值都需要更新計數器,增加了時間開銷

  3. 無法處理循環引用的問題,java的垃圾回收器沒有采用這個算法

循環引用例子

在這裏插入圖片描述

當p引用斷掉以後右邊的三個對象循環引用

如何解決循環引用

  1. 手動解除,再合適的時機手動解除引用關係

  2. 使用弱引用weakref(垃圾回收時會回收弱引用的對象)

2.2 可達性分析算法

可達性分析算法也稱根搜索算法和追蹤性垃圾收集

1. 簡述

  1. 從跟對象集合(GC Roots)爲起始點,從上至下搜索被根對象集合所連接的目標對象

  2. 內存中存活的對象都會被根對象集合直接或間接連着,搜索所走過的路徑被稱爲引用鏈(Reference Chain)

  3. 如果對象沒有任何引用鏈相連,則可以標記爲垃圾對象,反之則是存活的對象

    在這裏插入圖片描述

2. GC Roots有哪些

  1. 虛擬機棧中引用的對象
    比如:方法中使用到的參數和局部變量

  2. 本地方法棧內JNI引用的對象

  3. 方法區中類靜態屬性引用的對象
    比如:類的靜態變量

  4. 方法區中常量引用的對象
    比如:字符串常量池裏的引用

  5. 所有被同步鎖synchronized持有的對象

  6. 虛擬機內部引用

    6.1 Class對象

    6.2 常駐異常對象

    6.3 系統類加載器

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

  8. 除了上述固定的GC Roots集合以外,根據用戶所選用的垃圾回收器以及當前回收的內存區域的不同,還可以有其它對象“臨時性的加入”
    比如:回收新生代的時候,老年代指向新生代引用也會被當作Root

注意

使用可達性算法判斷內存是否可回收,必須在能保障一致性的快照中進行,這是導致GC進行時必須“Stop The World”的一個重要原因(並行收集的CMS在枚舉根節點時也要停頓)

3. 對象的finalization機制

基本概述

  1. Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷燬之前的自定義處理邏輯

  2. 垃圾回收對象之前,總會先調用這個對象的finalize()方法

  3. finalize()方法允許在子類中被重寫,用於對象在回收前的資源清理工作,如關閉文件,套接字和數據庫連接等

  4. 不要主動調用對象的finalize()方法,應該交給垃圾回收機制調用,主要原因

    4.1 在finalize()時可能會導致對象復活

    4.2 finalize()方法的執行時間時沒有保障,完全由GC線程決定,不發生GC,則finalize()沒有執行的機會

    4.3 一個糟糕的finalize()會嚴重影響GC的性能

2. 對象的三種狀態

由於finalize()方法的存在,虛擬機中的對象一般處於三種可能的狀態

  1. 可觸及的:從根節點開始,可以到達這的對象

  2. 可復活的:對象的所有引用都被釋放,但是對象有可能在finalize()中復活

  3. 不可觸及的:對象的finalize()被調用,並且沒有復活。不可觸及的對象不可能被複活,因爲finalize()只會被調用一次

3. 對象回收的具體過程

判定一個對象objA是否可回收,至少需要經歷兩次標記過程:

  1. 如果GC Roots到對象objA沒有引用鏈,則進行第一次標記

  2. 進行篩選,判斷此對象是否有必要執行finalize()方法

    2.1 若果objA沒有重寫finalize()方法,或者finalize()方法已經被虛擬機調用過,則虛擬機視爲“沒有必要執行”,objA被判定爲不可觸及的

    2.2 如果對象objA重寫了finalize()方法,且還未執行過那麼objA會被插入到F-Queue隊列,由一個虛擬機自動創建的、低優先級的Finalizer線程觸發其finalizer線程觸發其finalize()方法執行

    2.3 finalize()方法是對象逃脫死亡的最後機會,稍後GC會對F-Queue隊列中的對象進行第二次標記,如果objA在finalize()方法中與引用鏈上的任何一個對象建立了聯繫,那麼在第二次標記時,objA會被移出“即將回收”集合。之後,對象會再次出現沒有引用存在的情況,在這個情況下,finalize方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize方法只能被調用一次


    在這裏插入圖片描述

4. 垃圾清除階段算法

通過垃圾標記階段確認出哪些是垃圾以後,通過相關垃圾清除垃圾

4.1 標記-清除(Mark-Sweep)

1. 執行過程

當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也稱爲stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除

  1. 標記:Collector從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄爲可達對象。

  2. 清除:Collector對堆內存從頭到尾進行線性遍歷,如果發現某個對象在其Heaer中沒有標記可達對象,則將其回收


    在這裏插入圖片描述

2. 缺點

  1. 效率不算高(三個算法,屬於中間的)

  2. 在進行GC的時候,需要停止整個應用程序,導致用戶體驗差

  3. 這種方式清理出來的空閒內存是不連續的,產生內存碎片。需要維護空閒列表

何爲清除

這裏所謂的清除並不是真正的置空,而是把需要清除的對象地址保存在空閒的地址列表裏。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放。(再次存數據的時候,纔會真正的清除數據)

4.2 複製算法(Copying)

1. 核心思想

將活着的內存空間分爲兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中存活對象複製到未被使用的內存塊中,之後清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最後完成垃圾回收。


在這裏插入圖片描述

2. 優點

  1. 沒有標記和清除過程,實現簡單,運行高效

  2. 複製過去以後保證空間的連續性,不會出現內存碎片問題

3. 缺點

  1. 需要兩倍的內存空間

  2. 對於G1這種拆分爲大量region的GC,複製而不是移動,意味着GC需要維護region之間對象引用關係,不管是佔用或者時間開銷也不小。

  3. 如果系統中的垃圾對象很多,複製算法需要複製的對象數量並不會太大,反之需要複製的對象太多,會導致效率大大降低

4. 應用場景
在新生代中,對常規應用的垃圾回收,一次通常可以回收70%-99%的內存空間。回收性價比很高。所以現在的商業虛擬機都是用複製算法回收新生代


在這裏插入圖片描述


回收過程如下

  1. 在GC開始的時候,對象只會存在於Eden區和名爲“From”的Survivor區,Survivor區“To”是空的。緊接着進行GC,Eden區中所有存活的對象都會被複制到“To”

  2. 在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到老年代中

  3. 沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空

  4. “From”和“To”會交換他們的角色

4.3 標記-壓縮算法(Mark-Compact)

1. 執行過程

  1. 第一階段和標記清除算法一樣,從根節點開始標記所有被引用對象

  2. 第二階段將所有的存活對象壓縮到內存的一端,按順序排放,之後清理邊界外所有的空間


    在這裏插入圖片描述

2. 標記-壓縮與標記清除算法的比較

  1. 標記-壓縮算法的最終效果等同於標記-清除算法執行完成後,再進行一次內存碎片整理,因此也可以成爲標記-清除-壓縮算法(Mark-Sweep-Compact)

  2. 兩者的本質區別在於標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收後的存活對象是一項優缺點並存的風險決策,移動以後需要修改引用對象指向新的地址

  3. 標記壓縮算法對於標記的存活對象將會被整理,按照內存地址依次排列,未被標記的內存將會被清理掉。在給新對象分配內存時,jvm只需要持有一個內存的起始地址即可,比維護空閒列表簡單

3. 內存分配

  1. 指針碰撞

假設JVM堆中內存是規整的,所有用過的內存放在一邊,沒用過的內存放在另一邊,中間放着一個指針作爲分界點的指示器,那所分配內存的過程就僅僅是把那個指針向空閒空間的方向挪動一段與對象大小相等的距離,這種分配方式被稱爲“指針碰撞(Bump the Pointer)”

  1. 空閒列表

如果JVM堆中的內存不是規整的,使用過的內存空間與未使用的內存空間相互交錯,那就沒辦法進行簡單的“指針碰撞”了,虛擬機就必須維護一個列表,記錄哪些內存塊是可用的,分配的時候在列表中找到一段足夠大的內存空間分配給對象實例,並更新列表中的記錄,這種分配方式被稱爲“空閒列表(Free List)”

使用標記-清除算法內存空間是零散的所以後續使用空閒列表的方式爲對象分配內存空間,而標記清除-壓縮算法會壓縮空間,內存是規整的,後續使用標記壓縮算法分配內存空間

4. 優點

  1. 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,jvm只需要持有一個內存的起始地址即可(每個對象佔用的空間由內部成員變量決定,每個成員變量的空間java有規定,如int佔4個字節)

  2. 消除了複製算法當中,內存減半的高額代價

5. 缺點

  1. 從效率上來說,標記-整理算法要低於標記-清除算法,更低於複製算法

  2. 移動對象的同時,如果對象被其他對象引用,還需要調整引用引用的地址

  3. 移動過程中,則需要全程暫停用戶引用程序。即:STW

4.4 三種算法的比較

在這裏插入圖片描述

  1. 效率上來說,複製算法最快,但是浪費了太多的內存。

  2. 標記-壓縮比複製算法多了一個標記階段,比標記-清除算法多了一個整理內存的階段

5. 分代收集算法

1. 前言

首先分代收集算法不是指某一種具體的垃圾回收算法,是指在不同的內存代中使用不同的垃圾回收算法(上述三種)

2. 年輕代

  1. 年輕代特點,區域相對老年代小,對象生命週期短、存活率低、回收頻繁

  2. 年輕代適合使用複製算法,存活率低需要複製的存活對象不會很多,需要“浪費”一個survivor區的空間

3. 老年代

  1. 老年代特點,區域大,對象生命週期長、存活率高、回收不及年輕代頻繁

  2. 老年代使用標記-清除算法或者標記清除與標記-整理的混合實現

  • Mark階段的開銷與存活對象的數量成正比
  • Sweep階段的開銷與所管理的內存區域大小成正相關
  • Compact階段的開銷與存活對象的數據成正比

6. 補充算法

所謂的補充算法,和分代回收算法的概念一樣,只是在於如何運用上述的三種算法,並不是真正具體的垃圾回收算法

6.1 增量收集算法

  1. 如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集線程和運用線程交替執行

  2. 增量收集算法的基礎任是傳統的標記-清除和複製算法。增量收集算法通過對線程間的衝突的妥善處理,允許垃圾收集線程分階段的方式完成標記,清理或複製工作

  3. 缺點:垃圾回收過程中,間斷性的還執行了應用程序代碼,能減少系統的停頓時間,但是線程切換和上下文轉換需要消耗時間,會導致垃圾回收的總體成本上升,造成系統的吞吐量下降(運用線程耗時佔總耗時部分)

6.2 分區算法

  1. 一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間就越長,有關GC的停頓也越長,爲了更好的控制GC長生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓

  2. 從內存的角度,分代算法將按照對象的生命週期長短劃分成兩個部分,分區算法將整個堆空間劃分成連續的不同小空間

  3. 每一個小區間都獨立使用,獨立回收。這種算法的好處時可以控制一次回收多少個小區間


在這裏插入圖片描述

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