垃圾收集算法與垃圾收集器

如何判斷對象是否應該被回收?

在堆裏面存放着Java世界中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要肯定這些對象哪些應該被回收。java

引用計數算法

在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任什麼時候刻計數器爲零的對象就是不可能再被使用的。web

存在的問題:很難解決對象之間循環引用的問題。算法

主流的Java虛擬機都沒有選擇該方式來管理內存。緩存

可達性分析算法

基本思路:經過一系列稱爲「GC Roots」的根對象做爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過 程所走過的路徑稱爲「引用鏈」,若是某個對象到GC Roots間沒有任何引用鏈相連, 或者用圖論的話來講就是從GC Roots到這個對象不可達時,則證實此對象是不可能再被使用的。以下圖多線程

可達性分析算法
可達性分析算法

圖片來源:《深刻理解Java虛擬機併發

固定可做爲GC Roots的對象包括如下幾種:編輯器

  • 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的 參數、局部變量、臨時變量等。
  • 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
  • 在方法區中常量引用的對象,譬如字符串常量池裏的引用。
  • 在本地方法棧中Native方法引用的對象。
  • Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(好比 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
  • 全部被同步鎖(synchronized關鍵字)持有的對象。
  • 反映Java虛擬機內部狀況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等。

Java四種引用類型

在JDK 1.2版以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strongly Re-ference)、軟 引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強 度依次逐漸減弱。ide

Java引用類型
Java引用類型

finalize()方法

若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記,隨後進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那麼虛擬機將這兩種狀況都視爲「沒有必要執行」。post

對象能夠在finalize()方法中經過將本身與引用鏈上的對象從新關聯,來逃脫垃圾回收。測試

PS:至今沒寫過這個方法。不推薦使用。

/**  * 此代碼演示了兩點:  * 1.對象能夠在被GC時自我拯救。  * 2.這種自救的機會只有一次,由於一個對象的finalize()方法最多隻會被系統自動調用一次  *  * 代碼來源:《深刻理解Java虛擬機》  * @author : fuyuaaa  * @date : 2020-06-03 15:58  */ public class FinalizeEscapeGC {  public static FinalizeEscapeGC SAVE_HOOK = null;   public void isAlive() {  System.out.println("yes, i am still alive :)");  }   @Override  protected void finalize() throws Throwable {  super.finalize();  System.out.println("finalize method executed!");  FinalizeEscapeGC.SAVE_HOOK = this;  }   public static void main(String[] args) throws Throwable {  SAVE_HOOK = new FinalizeEscapeGC();  //對象第一次成功拯救本身  SAVE_HOOK = null;  System.gc();  // 由於Finalizer方法優先級很低,暫停0.5秒,以等待它  Thread.sleep(500);  if (SAVE_HOOK != null) {  SAVE_HOOK.isAlive();  } else {  System.out.println("no, i am dead :(");  }    // 下面這段代碼與上面的徹底相同,可是此次自救卻失敗了,由於finalize只會被執行一次  SAVE_HOOK = null;  System.gc();  // 由於Finalizer方法優先級很低,暫停0.5秒,以等待它  Thread.sleep(500);  if (SAVE_HOOK != null) {  SAVE_HOOK.isAlive();  } else {  System.out.println("no, i am dead :(");  }  } }  result: finalize method executed! yes, i am still alive :) no, i am dead :( 複製代碼

方法區的垃圾收集

方法區的垃圾收集主要回收兩部份內容:

廢棄的常量:沒有地方引用這個常量

再也不使用的類型:(同時知足三個條件)

  • 該類全部的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
  • 加載該類的類加載器已經被回收。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

垃圾收集算法

標記-清除算法

首先標記出全部須要回收的對象,在標記完成後,統一回收掉全部被標記的對象;也能夠反過來,標記存活的對象,統一回收全部未被標記的對象。

缺點

  • 執行效率不穩定,標記和清除兩個過程的執行效率都隨對象數量增加而下降。
  • 內存空間的碎片化問題,可能會致使大對象沒法分配內存而不得不提早gc。

標記-複製算法

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

缺點

  • 將會產生大量的內存間複製的開銷
  • 浪費一半空間

如今的商用Java虛擬機大多都優先採用了這種收集算法去回收新生代。

標記-整理算法

標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向內存空間一端移動,而後直接清理掉邊界之外的內存。

缺點

  • 若是有大量對象存活,移動存活對象並更新全部引用這些對象的地方將會是一種極爲負重的操做,並且這種對象移動操做必須全程暫停用戶應用程序才能進行。

經典垃圾收集器

HotSpot虛擬機的垃圾收集器
HotSpot虛擬機的垃圾收集器

PS:

  • 上圖爲HotSpot虛擬機的。有連線表明能夠搭配使用。

  • 因爲維護和兼容性測試的成本,在JDK 8時將Serial+CMS、 ParNew+Serial Old這兩個組合聲明爲廢棄(JEP 173),並在JDK 9中徹底取消了這些組合的支持(JEP 214)。

  • 並行(Parallel):並行描述的是多條垃圾收集器線程之間的關係,說明同一時間有多條這樣的線程在協同工做,一般默認此時用戶線程是處於等待狀態。

  • 併發(Concurrent):併發描述的是垃圾收集器線程與用戶線程之間的關係,說明同一時間垃圾收集器線程與用戶線程都在運行。因爲用戶線程並未被凍結,因此程序仍然能響應服務請求,但因爲垃圾收集器線程佔用了一部分系統資源,此時應用程序的處理的吞吐量將受到必定影響。

Serial 收集器

  • 最基礎、歷史最悠久
  • 新生代收集器
  • 單線程
  • 標記-複製算法
  • 會形成「Stop The World」
Serial:Serial Old收集器運行示意圖
Serial:Serial Old收集器運行示意圖

ParNew 收集器

  • Serial垃圾收集器的多線程並行版本
  • 新生代收集器
  • 多線程並行
  • 標記-複製算法
  • 會形成「Stop The World」
ParNew:SerialOld收集器運行示意圖
ParNew:SerialOld收集器運行示意圖

Parallel Scavenge 收集器

  • 新生代收集器
  • 多線程並行
  • 標記-複製算法
  • 會形成「Stop The World」
  • 收集器目標是達到一個可控制的吞吐量
    • 控制最大垃圾收集停頓時間 -XX:MaxGCPauseMillis
    • 直接設置吞吐量大小 -XX:GCTimeRatio
    • 自適應的調節策略 -XX:+UseAdaptiveSizePolicy

Serial Old 垃圾收集器

  • Serial收集器的老年代版本
  • 單線程
  • 標記-整理算法
  • 會形成「Stop The World」
Serial:SerialOld收集器運行示意圖2
Serial:SerialOld收集器運行示意圖2

Paraller Old 垃圾收集器

  • Parallel Scavenge收集器的老年代版本
  • 多線程併發
  • 標記-整理算法
  • 始於JDK6
ConcurrentMarkSweep收集器運行示意圖
ConcurrentMarkSweep收集器運行示意圖

CMS 收集器

  • 標記-清除算法
  • 以獲取最短回收停頓時間爲目標
ConcurrentMarkSweep收集器運行示意圖
ConcurrentMarkSweep收集器運行示意圖

具體步驟:

CMS收集器步驟
  • 優勢:
    • 併發收集、低停頓
  • 缺點:
    • 對處理器資源敏感,在併發階段會佔用線程致使總吞吐量變低。
    • 沒法處理「浮動垃圾」(併發階段產生的新的垃圾)。
    • gc時須要預留內存空間給用戶線程,若是這個內存不夠,會致使「併發失敗」,會臨時使用Serial Old收集器進行老年代收集(會「Stop The World」)。
    • 標記-清除,沒法處理空間碎片(CMS默認開啓了內存碎片的合併整理過程)。

Garbage First 收集器

G1把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每個Region均可以扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器可以對扮演不一樣角色的 Region採用不一樣的策略去處理。

Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認爲只要大小超過了一個 Region容量一半的對象便可斷定爲大對象。

可預測的停頓時間模型:將Region做爲單次回收的最小單元,讓G1收集器去跟蹤各個Region裏面的垃圾堆積的「價值」大小,價值即回收所得到的空間大小以及回收所需時間的經驗值,而後在後臺維護一個優先級列表,每次根據用戶設定容許的收集停頓時間,優先處理回收價值收益最大的那些Region,這也就是「Garbage First」名字的由來。

G1收集器運行示意圖
G1收集器運行示意圖

具體步驟:

G1收集器步驟

PS

  • TAMS:在收集線程與用戶線程同時運行的時候,確定會有新對象的分配。G1爲每個Region設 計了兩個名爲TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用於併發回收過程當中的新對象分配,併發回收時新分配的對象地址都必需要在這兩個指針位置以上。
  • STAB:原始快照算法,用於處理併發時可能產生的「對象」消失問題。能夠瞅瞅這個: 連接
  • 與CMS相似,若是內存回收的速度趕不上內存分配的速度,G1收集器也要被迫凍結用戶線程執行,致使Full GC(多線程)而產生長時間「Stop The World」。

參考

深刻理解Java虛擬機

圖片來源《深刻理解Java虛擬機

本文使用 mdnice 排版

相關文章
相關標籤/搜索