天天看點

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

本文目錄

  • 概念
  • 1.1 如何識别垃圾
    • 1.1.1 Reference count 引用計數
    • 1.2.2 Root Searching 根搜尋/根可達
  • 2 垃圾回收算法
    • 2.1 Mark-Sweep 标記-清除算法
    • 2.2 Copying 複制算法
    • 2.3 Mark-compact 标記整理算法
  • 3 分代收集算法
  • 4 垃圾收集器
    • 4.1 Serial 年輕代 串行回收
    • 4.2 ParNew收集器
    • 4.3 Parallel Scavenge收集器
    • 4.4 CMS收集器
    • 4.5 G1收集器
  • 如何選擇垃圾收集器
  • 關于垃圾回收機制的關鍵點

概念

對于某個對象而言,隻要應用程式中持有該對象的引用,就說明該對象不是垃圾。當程式運作到某一階段的時候,這個對象沒有任何指針對其引用,那麼它就是垃圾。

1.1 如何識别垃圾

1.1.1 Reference count 引用計數

判斷這個對象沒有被引用,那麼它就是垃圾。這個方法實作簡單,效率高,但是目前主流的虛拟機中并沒有選擇這個算法來管理記憶體,主要它存在對象之間互相循環引用的問題。

1.2.2 Root Searching 根搜尋/根可達

能作為GC ROOTs根節點有: 線程棧的本地變量,靜态變量,本地方法棧變量,常量引用,類加載器。

通過GC ROOTS,這些GC ROOTS向下搜尋,看對象是否可達,找到的對象都标記為非垃圾對象,其餘未标記的對象都是垃圾對象。

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

2 垃圾回收算法

2.1 Mark-Sweep 标記-清除算法

首先标記可以回收的對象,标記完成後統一回收所有被标記的對象。這種算法在清除後會産生大量不連續的記憶體碎片。

例如:回收前

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

回收後:

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

2.2 Copying 複制算法

将記憶體分成大小相同的兩塊,一次隻使用其中的一塊。當一塊記憶體使用完成後,就将存活的對象複制到另一塊記憶體塊去,然後在把使用的空間一次性清理掉。缺點:記憶體的使用率低,浪費記憶體

垃圾回收前:

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

垃圾回收後:

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

2.3 Mark-compact 标記整理算法

首先标記存活對象,然後讓所有的存活對象向一端移動,然後清除掉所有的邊界以外的記憶體。效率低!

處理前

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

處理後

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

3 分代收集算法

目前的虛拟機的垃圾收集都采用分代收集算法,根據對象的存活周期不同将記憶體分為幾塊。一般分為新生代和老年代。更具各個年代的特點選擇合适的垃圾收集算法。

在新生代,大量的對象存活時間很短,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的,所有一般選擇"标記清除"或者“标記整理”算法經行垃圾收集。标記清除或者标記整理算法比複制算法慢10倍以上。

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

MinorGC = YGC 新生代垃圾回收

MajorGC = FGC 老年代垃圾回收,應該盡量減少盡量減少FGC

1). 新生代 = Eden + 2個suvivor區

YGC回收之後,大多數的對象會被回收,活着的進入s0

再次YGC,活着的對象eden + s0 -> s1

再次YGC,eden + s1 -> s0

年齡足夠 -> 老年代 (15 CMS 6)

s區裝不下 -> 老年代

2). 老年代

頑固分子 年代滿了FGC Full GC

4 垃圾收集器

垃圾回收器的發展路線,是随着記憶體越來越大的過程而演進.從分代算法演化到不分代算法

Serial算法 幾十兆

Parallel算法 幾個G

CMS 幾十個G - 承上啟下,開始并發回收

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

4.1 Serial 年輕代 串行回收

Serial(串行)收集器是最基本、曆史最悠久的垃圾收集器了。是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它隻會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( Stop The World),直到它收集結束。

單機cpu效率最高,虛拟機是client模式的預設垃圾回收器

新生代采用複制算法,老年代采用标記整理算法

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

Serial收集器由于沒有線程互動的開銷,自然可以獲得很高的單線程收集效率。

Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單線程收集器。

它主要有兩大用 途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是 作為CMS收集器的後備方案。

4.2 ParNew收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為 (控制參數、收集算法、回收政策等等)和Serial收集器完全一樣。使用:-XX:UserParNewGC

預設的收集線程數跟cpu核數 相同,當然也可以用參數(-XX:ParallelGCThreads)指定收集線程數,但是一般不推薦修改

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

它是許多運作在Server模式下的虛拟機的首要選擇,隻有它能與CMS收集器 (真正意義上的并發收集器,後面會介紹到)配合工作。

4.3 Parallel Scavenge收集器

Parallel Scavenge 收集器類似于ParNew 收集器,是Server 模式(記憶體大于2G,2個cpu)下的預設收集器。

使用: -XX:+UseParallelGC(年輕代),- XX:+UseParallelOldGC(老年代)。

Parallel Scavenge收集器關注點是**吞吐量(**高效率的利用CPU)。CMS等垃圾收集器的關注點 更多的是使用者線程的停頓時間(提高使用者體驗)。

所謂吞吐量就是CPU中用于運作使用者代碼的時 間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供使用者找到最合适的停頓時間或最大吞吐量。

新生代采用複制算法,老年代采用标記-整理算法。

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

4.4 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。它非常符合在注重使用者體驗的應用上使用,它是HotSpot虛拟機第一款真正意義上的并發收集器, 它第一次實作了讓垃圾收集線程與使用者線程(基本上)同時工作。使用:-XX:+UseConcMarkSweepGC(old)

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “标記-清除”算法實作的,整個過程分為四個步驟:

初始标記 : 暫停所有其他線程Stop The Word,并記錄gc roots能直接引用的對象,速度很快。

并發标記 :同時開啟GC和使用者線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構并不能保證包含目前所有的可達對象。因為使用者線程可能會不斷的更新引用域,是以GC線程無法保證可達性分析的實時性。是以這個算法裡會跟蹤記錄這些發生引 用更新的地方。

重新标記 :修改并發标記因使用者程式變動的内容 Stop the World

并發清理 :開啟使用者線程,同時GC線程開始對未标記的區域做清掃。

由于整個過程中,并發标記和并發清除,收集器線程可以與使用者線程一起工作,是以總體上來 說,CMS收集器的記憶體回收過程是與使用者線程一起并發地執行的。

優點 :并發收集,停頓低

缺點:

1.對CPU資源敏感(會和服務搶資源)

2.無法處理浮動垃圾(在并發清理階段産生垃圾,這種浮動垃圾隻能等下次gc再經行清理)

3.使用标記-清除算法,會導緻大量的空間碎片産生,當然 通過參數-XX:+UseCMSCompactAtFullCollection可以讓jvm在執行完标記清除後再做整理

4 .執行過程中的不确定性,會存在上一次垃圾回收還沒執行完,然後垃圾回收又被觸 發的情況,特别是在并發标記和并發清理階段會出現,一邊回收,系統一邊運作,也許沒回 收完就再次觸發full gc,也就是"concurrent mode failure",此時會進入stop the world,用serial old垃圾收集器來回收

4.5 G1收集器

G1 (Garbage-First) 是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高機率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征。使用: -XX:UseG1GC

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

G1将Java堆劃分為多個的獨立區域(Region),JVM最多可以有2048個Region。 一可以 用參數"-XX:G1HeapRegionSize"手動指定Region大小,但是推薦預設的計算方式。 G1保留了年輕代和老年代的概念,邏輯上分代,但是實體上不分代。

預設年輕代對堆記憶體的占比是5%,可以通過“-XX:G1NewSizePercent”設定新生代初始占比,在系統 運作中,JVM會不停的給年輕代增加更多的Region,但是最多新生代的占比不會超過60%,可以 通過“-XX:G1MaxNewSizePercent”調整。年輕代中的Eden和Survivor對應的region也跟之前 一樣,預設8:1:1,假設年輕代現在有1000個region,eden區對應800個,s0對應100個,s1對應 100個。

一個Region可能之前是年輕代,如果Region進行了垃圾回收,之後可能又會變成老年代,也就是說Region的區域功能可能會動态變化。

G1有專門配置設定大對象的Region叫Humongous區。在G1中,大對象的判定規則就是一個大對象超過了一個Region大小的50%,而且 一個大對象如果太大,可能會橫跨多個Region來存放。Humongous區專門存放短期巨型對象,不用直接進老年代,可以節約老年代的空間,避免因為老 年代空間不夠的GC開銷。 Full GC的時候除了收集年輕代和老年代之外,也會将Humongous區一并回收。

G1收集器一次GC的運作過程大緻分為以下幾個步驟:

**初始标記(initial mark ,STW):**暫停所有的其他線程,并記錄下gc roots直接能引用 的對象,速度很快

并發标記 (Concurrent Marking): 從GC Roots進行可達性分析,找出存活的對象,與使用者線程并發執行

**最終标記(Final Marking STW)😗*修正在并發标記階段因為使用者程式的并發執行導緻變動的資料,需暫停使用者線程

**篩選回收(Live Data Counting and Evacuation,STW):**對各個Region的回收價值和成本進行排序,根據 使用者所期望的GC停頓時間制定回收計劃

比如說老年代此時有1000個Region都滿了,但是因為根據預期停頓時間,本次垃圾回收可能隻能停頓200毫秒,那麼通過之前回收成本計算得知,可能回收其中800個 Region剛好需要200ms,那麼就隻會回收800個Region,盡量把GC導緻的停頓時間控制在 我們指定的範圍内。這個階段其實也可以做到與使用者程式一起并發執行,但是因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅提高收集效率。不管是年輕代 或是老年代,回收算法主要用的是複制算法(局部,從整體上看是标記整理算法),将一個region中的存活對象複制到另一個 region中,G1采用複制 算法回收幾乎不會有太多記憶體碎片。

關于JVM垃圾回收的一些筆記概念1.1 如何識别垃圾2 垃圾回收算法3 分代收集算法4 垃圾收集器如何選擇垃圾收集器關于垃圾回收機制的關鍵點

G1收集器在背景維護了一個優先清單,每次根據允許的收集時間,優先選擇回收價值最大的 Region(這也就是它的名字Garbage-First的由來),比如一個Region花200ms能回收10M垃圾,另外一個Region花50ms能回收20M垃圾,在回收時間有限情況下,G1當然會優先選擇後面這個Region回收。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集 器在有限時間内可以盡可能高的收集效率。

G1 收集器的特點

**分代收集:**雖然G1可以不需要其他收集器配合就能獨立管理整個堆,但是還是保留了分代的概念

空間整合: 與CMS的“标記–清除” 算法不同,G1整體來看是基于“标記整理”算法實作的收集器;從局部上看是“複制”算法實作的

**可預測的停頓:**這是G1相對于CMS的另一個大優勢,降低停頓時間是G1 和 CMS 共同的關注點,但G1 除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指 定在一個長度為M毫秒的時間片段(通過參數"-XX:MaxGCPauseMillis"指定)内完成垃圾收集。

停頓時間:垃圾收集器進行垃圾回收終端應用執行響應的時間

吞吐量:運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)

停頓時間越短就越适合需要和使用者互動的程式,良好的響應速度能提升使用者體驗;

高吞吐量則可以高效地利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任 務。

G1 垃圾收集分類

  1. YoungGC :YoungGC并不是說現有的Eden區放滿了就會馬上觸發,而且G1會計算下現在Eden區回收大 概要多久時間,如果回收時間遠遠小于參數 -XX:MaxGCPauseMills 設定的值,那麼增加年輕代 的region,繼續給新對象存放,不會馬上做Young GC,直到下一次Eden區放 滿,G1計算回收時 間接近參數 -XX:MaxGCPauseMills 設定的值,那麼就會觸發Young GC。
  2. MixedGC:不是FullGC,老年代的堆占有率達到參數(-XX:InitiatingHeapOccupancyPercen)設定的值 則觸發,回收所有的Young和部分Old(根據期望的GC停頓時間确定old區垃圾收集的優先順序)以 及大對象區,正常情況G1的垃圾收集是先做MixedGC,主要使用複制算法,需要把各個region中 存活的對象拷貝到别的region裡去,拷貝過程中如果發現沒有足夠的空region能夠承載拷貝對象 就會觸發一次Full GC
  3. Full GC:停止系統程式,然後采用單線程進行标記、清理和壓縮整理,好空閑出來一批Region來供下 一次MixedGC使用,這個過程是非常耗時的。

G1 垃圾收集器優化建議

假設參數 -XX:MaxGCPauseMills 設定的值很大,導緻系統運作很久,年輕代可能都占用了堆 記憶體的60%了,此時才觸發年輕代gc。 那麼存活下來的對象可能就會很多,此時就會導緻Survivor區域放不下那麼多的對象,就會進 入老年代中。

或者是你年輕代gc過後,存活下來的對象過多,導緻進入Survivor區域後觸發了動态年齡判定 規則,達到了Survivor區域的50%,也會快速導緻一些對象進入老年代中。

是以這裡核心還是在于調節 -XX:MaxGCPauseMills 這個參數的值,在保證他的年輕代gc别太 頻繁的同時,還得考慮每次gc過後的存活對象有多少,避免存活對象太多快速進入老年代,頻繁觸 發mixed gc.

如何選擇垃圾收集器

  1. 優先調整堆的大小讓伺服器自己來選擇
  2. 如果記憶體小于100M,使用串行收集器
  3. 如果是單核,并且沒有停頓時間要求,使用串行或JVM自己選
  4. 如果允許停頓時間超過1秒,選擇并行或JVM自己選
  5. 如果響應時間最重要,并且不能超過1秒,使用并發收集器
  6. 串行收集器->Serial和Serial Old 隻能有一個垃圾回收線程執行,使用者線程暫停。 适用于記憶體比較小的嵌入式裝置
  7. 并行收集器[吞吐量優先]->Parallel Scanvenge、Parallel Old 多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态。 适用于科學計算、背景處理等若互動場景
  8. 并發收集器[停頓時間優先]->CMS、G1 使用者線程和垃圾收集線程同時執行(但并不一定是并行的,可能是交替執行的),垃圾收集線程在執行的 時候不會停頓使用者線程的運作。 适用于相對時間有要求的場景,比如Web

關于垃圾回收機制的關鍵點

  1. 隻回收JVM堆記憶體中的對象空間;
  2. 堆其他實體連接配接,比如資料庫連接配接,輸入流、輸出流,Socket連接配接無能為力;
  3. JVM有多種垃圾回收機制實作算法,變現各異;
  4. 垃圾回收機制具有不可預知性,程式無法精确控制垃圾回收機制的執行;
  5. 可以将應用變量的值設定為null,暗示垃圾回收機制可以回收該對象;
  6. 可以通過System.gc()或者Runtime.getRuntime().gc()來通知系統進行垃圾回收,會有一些效果,但是系統是否進行垃圾回收依然不确定;
  7. 垃圾回收機制回收任何對象之前,總會先調用它的finalize方法(如果覆寫該方法,讓一個新的引用變量重新引用該變量,則會重新激活對象)。