JVM 垃圾回收算法和垃圾回收器


JVM 垃圾回收算法和垃圾回收器。

作者:IT王小二
博客:https://itwxe.com

一、垃圾回收的區域

  • 棧:棧中的生命周期是跟隨線程,所以一般不需要關注。
  • 堆:堆中的對象是垃圾回收的重點。
  • 方法區:這一塊也會發生垃圾回收,不過這塊的效率比較低,一般不是我們關注的重點。

二、怎么判斷對象的存活

一般有兩種方式(引用計數法、可達性分析),JVM使用的是可達性分析

1. 引用計數法

給對象添加一個引用計數器,當對象增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數為 0 的對象可被回收(Python 在用,但主流虛擬機沒有使用)。

  • 優點:快,方便,實現簡單。
  • 缺陷:對象相互引用時(A.instance=B 同時 B.instance=A),很難判斷對象是否該回收。

2. 可達性分析

來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。

作為 GC Roots 的對象包括下面幾種:

  • 當前虛擬機棧中局部變量表中的引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中的常量引用的對象

3. finalize

Java提供finalize()方法,垃圾回收器准備釋放內存的時候,會先調用finalize(),可以完成對象的拯救(不被回收),但是不能保證一定不被回收,說白了就是沒啥用,一個坑。

三、各種引用(Reference)

Reference 中存儲的數據代表的是另一塊內存的起始地址。

1. 強引用

一般的 Object obj = new Object() ,就屬於強引用。

(如果有 GCroots 的強引用)垃圾回收器絕對不會回收它,當內存不足時寧願拋出 OOM 錯誤,使得程序異常停止,也不會回收強引用對象。

2. 軟引用

SoftReference垃圾回收器在內存充足的時候不會回收它,而在內存不足時會回收它。

示例代碼:

public static void main(String[] args) {
    String str = new String("SunnyBear"); // 強引用
    SoftReference<String> strSoft = new SoftReference<String>(str);
    str = null; // 干掉強引用,確保只有strSoft的軟引用
    System.out.println(strSoft.get()); // SunnyBear
    System.gc(); // 執行一次gc,此命令請勿在線上使用,僅作示例操作
    System.out.println("------------ gc after");
    System.out.println(str); // null
    System.out.println(strSoft.get()); // SunnyBear
}

所以軟引用一般用來實現一些內存敏感的緩存,只要內存空間足夠,對象就會保持不被回收掉

3. 弱引用 WeakReference

垃圾回收器在掃描到該對象時,無論內存充足與否,都會回收該對象的內存

示例代碼:

public static void main(String[] args) {
    String str = new String("SunnyBear"); // 強引用
    WeakReference<String> strWeak = new WeakReference<String>(str);
    str = null; // 干掉強引用,確保只有strSoft的軟引用
    System.out.println(strWeak.get()); // SunnyBear
    System.gc(); // 執行一次gc,此命令請勿在線上使用,僅作示例操作
    System.out.println("------------ gc after"); // null
    System.out.println(str); // null
    System.out.println(strWeak.get()); // null
}

實際應用,如 WeakHashMap、ThreadLocal。

4. 虛引用 PhantomReference

幽靈引用,最弱,被垃圾回收的時候收到一個通知,如果一個對象只具有虛引用,那么它和沒有任何引用一樣,任何時候都可能被回收。

虛引用主要用來跟蹤對象被垃圾回收器回收的活動。

四、GC

1. Minor GC

  • 特點: 發生在新生代上,發生的較頻繁,執行速度較快。
  • 觸發條件: Eden 區空間不足/空間分配擔保。

2. Full GC

  • 特點:主要發生在老年代上(新生代也會回收),較少發生,執行速度較慢。
  • 觸發條件:
    • 調用 System.gc() 。
    • 老年代區域空間不足。
    • 空間分配擔保失敗。
    • JDK 1.7 及以前的永久代(方法區)空間不足。

五、垃圾回收算法

1. 復制算法(Copying)

將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半。

復制算法

  • 優點
    • 簡單高效,不會出現內存碎片。
  • 缺點
    • 內存利用率低。
    • 存活對象較多時效率明顯降低,因為需要移動每個不可回收數據的內存實際位置。

注:

專門研究表明,新生代中的對象 90%是“朝生夕死”的,所以一般來說回收占據 10% 的空間夠用了,所以並不需要按照 1:1 的比例來划分內存空間,而是將內存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor[1]。當回收時,將 Eden 和 Survivor 中還存活着的對象一次性地復制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。
HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用內存空間為整個新生代容量的 90%(80%+10%),只有 10%的內存會被“浪費”。

2. 標記-清除算法(Mark-Sweep)

首先標記所有需要回收的對象,然后統一回收被標記的對象。

標記清除算法

  • 優點
    • 利用率100% 。
  • 缺點
    • 標記和清除效率都不高(對比復制算法)。
    • 會產生大量不連續的內存碎片。

3. 標記-整理算法(Mark-compact)

首先標記出所有需要回收的對象,在標記完成后,后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端,邊界以外的內存。

標記整理算法

  • 優點
    • 利用率100% 。
    • 沒有內存碎片。
  • 缺點
    • 標記和清除效率都不高(對比復制算法及標記清楚算法)。

六、垃圾回收器

jvm 垃圾回收器把上面的三種算法全部用到了,采用分代收集。

1、新生代:復制算法。

收集器 收集對象和算法 收集器類型
Serial 新生代,復制算法 單線程
ParNew 新生代,復制算法 並行的多線程收集器
Parallel Scavenge 新生代,復制算法 並行的多線程收集器

2、老年代:標記清除算法和標記整理算法

收集器 收集對象和算法 收集器類型
Serial Old 老年代,標記整理算法 單線程
Parallel Old 老年代,標記整理算法 並行的多線程收集器
CMS(Conc Mark Sweep ) 老年代,標記清除算法 並行和並發收集器
G1(Garbage First) 跨新生代和老年代,復制算法 + 標記整理算法 並行和並發收集器

注:

  • 並行:垃圾收集的多線程的同時進行。
  • 並發:垃圾收集的多線程和用戶應用的多線程同時進行。
  • 使用 jps -v 可以看到使用的垃圾收集器,例如:-XX:+UseConcMarkSweepGC (CMS)

1. 可以配套使用的垃圾回收器

連線表示可以 新生代老年代 配套使用的垃圾收集器。

垃圾回收器

2. Serial/Serial Old

最古老的,單線程,獨占式,成熟,適合單 CPU 服務器。-XX:+UseSerialGC 新生代和老年代都用串行收集器。

3. ParNew

ParNew 和 Serial 基本沒區別,唯一的區別:多線程,多 CPU 的,停頓時間比 Serial 少 。

-XX:+UseParNewGC 新生代使用 ParNew,老年代使用 Serial Old 。

可以和CMS搭配使用。

4. Parallel Scavenge(ParallerGC)/Parallel Old

關注吞吐量的垃圾收集器,高吞吐量則可以高效率地利用 CPU 時間,盡快完成程序的運算任務,主要適合在后台運算而不需要太多交互的任務。所謂吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了 100 分鍾,其中垃圾收集花掉 1 分鍾,那有吞吐效率就是 99% 。

5. CMS(Concurrent Mark Sweep)

收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的 Java 應用集中在互聯網站或者 B/S 系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。

CMS 收集器就非常符合這類應用的需求。-XX:+UseConcMarkSweepGC一般新生代使用 ParNew,老年代的用 CMS,從名字(包含“Mark Sweep”)上就可以看出,CMS 收集器是基於“標記—清除”算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些。

回收過程

CMS回收過程

整個過程分為 4 個步驟,包括:

1、初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快,需要停頓(STW -Stop the world)。

2、並發標記:從 GC Root 開始對堆中對象進行可達性分析,找到存活對象,它在整個回收過程中耗時最長,不需要停頓。

3、重新標記:為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,需要停頓(STW)。這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

  1. 並發清除:不需要停頓。

優缺點

1、優點

由於整個過程中耗時最長的並發標記和並發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS 收集器的內存回收過程是與用戶線程一起並發執行的

2、缺點

  • CPU 資源敏感:因為並發階段多線程占據 CPU 資源,如果 CPU 資源不足,效率會明顯降低。
  • 由於 CMS 並發清理階段 用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS 無法在當次收集中處理掉它們,只好留待下一次 GC 時再清理掉。這一部分垃圾就稱為 浮動垃圾
  • 由於浮動垃圾的存在,因此需要預留出一部分內存,意味着 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。在 1.6 的版本中老年代空間使用率閾值(92%),如果預留的內存不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機將臨時啟用 Serial Old 來替代 CMS。
  • 會產生內存碎片:標記-清除算法 會導致產生不連續的內存碎片。

6. G1

G1相比較CMS的改進

  • 基於標記-整理算法, 不會產生空間碎片,分配大對象時不會無法得到連續的空間而提前觸發一次full gc 。
  • 停頓時間可控: G1可以通過設置預期停頓時間(Pause time)來控制垃圾收集時間,但是這個預期停頓時間G1只能盡量做到,而不是一定能做到

可預測的停頓:

G1 收集器之所以能建立可預測的停頓時間模型,是因為它可以有計划地避免在整個 Java 堆中進行全區域的垃圾收集。G1 跟蹤各個 Region 里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region(這也就是 Garbage-First 名稱的來由)。這種使用 Region 划分內存空間以及有優先級的區域回收方式,保證了 G1 收集器在有限的時間內可以獲取盡可高的收集效率。

G1的設置參數

  • -XX:+UseG1GC // 開啟G1
  • -XX:MaxGCPauseMillis=200 // 預期停頓時間200毫秒,默認也是200
  • -XX:G1HeapRegionSize=2 // 設置每個區域大小2M,其必須是2的冪,范圍允許為1Mb到32Mb
  • -XX:G1NewSizePercent // 新生代最小值,默認值 5%
  • -XX:G1MaxNewSizePercent // 新生代最大值,默認值 60%
  • -XX:ParallelGCThreads // STW 期間,並行 GC 線程數
  • -XX:ConcGCThreads=n // 並發標記階段,並行執行的線程數

G1是怎么划堆內存的呢

G1 把堆划分成多個大小相等的 獨立區域(Region),新生代和老年代不再物理隔離

G1 算法將堆划分為若干個獨立區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者 Survivor 空間。例如其中一個獨立區域如圖:

獨立區域

GC 的模式

1、Young GC

Young GC 主要是對 Eden 區進行 GC,它在 Eden 空間耗盡時會被觸發。在這種情況下,Eden 空間的數據移動到 Survivor 空間中,如果 Survivor 空間不夠,Eden 空間的部分數據會直接晉升到老年代空間。Survivor 區的數據移動到新的 Survivor 區中,也有部分數據晉升到老年代空間中。最終 Eden 空間的數據為空,GC 停止工作,應用線程繼續執行

2、Mixed GC

選定所有新生代里的 Region,外加根據 global concurrent marking 統計得出收集收益高的若干老年代 Region。在用戶指定的開銷目標范圍內盡可能選擇益高的老年代 Region。Mixed GC 不是 full GC,它只能回收部分老年代的 Region。如果 mixed GC 實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行 Mixed GC,就會使用 serial old GC(full GC)來收集整個 GC heap。所以我們可以知道,G1 是不提供 full GC 的

收集過程

G1回收過程

大致分為4個步驟:

1、初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,並且修改 TAMS(Nest Top Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可以的 Region 中創建對象,此階段需要停頓線程(STW),但耗時很短

2、並發標記:從 GC Root 開始對堆中對象進行可達性分析,找到存活對象,此階段耗時較長,但可與用戶程序並發執行

3、最終標記:為了修正在並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 里面,最終標記階段需要把 Remembered Set Logs 的數據合並到 Remembered Set 中。這階段需要停頓線程(STW),但是可並行執行

4、篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計划。此階段其實也可以做到與用戶程序一起並發執行,但是因為只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅度提高收集效率

七、垃圾回收器的一些重要參數

參數 描述
UseSerialGC 虛擬機運行在 Client 模式下的默認值,打開此開關后,使用 Serial+Serial Old 的收集器組合進行內存回收
UseParNewGC 打開此開關后,使用 ParNew + Serial Old 的收集器組合進行內存回收
UseConcMarkSweepGC 打開此開關后,使用 ParNew + CMS + Serial Old 的收集器組合進行內存回收。Serial Old 收集器將作為 CMS 收集器出現 Concurrent Mode Failure 失敗后的后備收集器使用
UseParallelGC 虛擬機運行在 Server 模式下的默認值,打開此開關后,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器組合進行內存回收
UseParallelOldGC 打開此開關后,使用 Parallel Scavenge + Parallel Old 的收集器組合進行內存回收
SurvivorRatio 新生代中 Eden 區域與 Survivor 區域的容量比值,默認為 8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晉升到老年代的對象大小,設置這個參數后,大於這個參數的對象將直接在老年代分配
MaxTenuringThreshold 晉升到老年代的對象年齡,每個對象在堅持過一次 Minor GC 之后,年齡就增加 1,當超過這個參數值時就進入老年代
UseAdaptiveSizePolicy 動態調整 Java 堆中各個區域的大小以及進入老年代的年齡
HandlePromotionFailure 是否允許分配擔保失敗,即老年代的剩余空間不足以應付新生代的整個 Eden 和 Survivor 區的所有對象都存活的極端情況
ParallelGCThreads 設置並行 GC 時進行內存回收的線程數
GCTimeRatio GC 時間占總時間的比率,默認值為 99,即允許 1% 的 GC 時間,僅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 設置 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效
CMSInitiatingOccupancyFraction 設置 CMS 收集器在老年代空間被使用多少后觸發垃圾收集,默認值為 68%,僅在使用 CMS 收集器時生效
UseCMSCompactAtFullCollection 設置 CMS 收集器在完成垃圾收集后是否要進行一次內存碎片整理,僅在使用 CMS 收集器時生效
CMSFullGCsBeforeCompaction 設置 CMS 收集器在進行若干次垃圾收集后再啟動一次內存碎片整理,僅在使用 CMS 收集器時生效

都讀到這里了,來個 點贊、評論、關注、收藏 吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM