前言
本文是筆者JVM系列的第二篇文章,盡我所能将Java堆的分代、垃圾收集算法與垃圾收集器講出來,如果閱讀過程中遇到突然出現的術語,請先參考【七、術語參考】,如文中有錯誤或表述不準确的地方,感謝評論指出。
一、垃圾回收概念
對象配置設定記憶體在堆上,當對象不再使用,它們就變成了垃圾,需要被清理以釋放記憶體,這個過程稱作垃圾收集(簡稱GC,Garbage Collection)。
二、對象是否死亡
垃圾回收需要确定哪些對象是不可用的,是以需要确定對象已“死”,針對對象判活有以下兩種算法:
-
:一個對象每被其他方法或對象引用,就為它的引用數+1,不再引用時引用數-1,當對象引用數為0時,對象已死。引用計數算法
-
:基本思想是通過一系列可以作為GC Roots的對象作為起始點,向下搜尋被關聯的對象,當可達性分析完成、還未被關聯的對象已死。可達性分析
- 引用計數算法很難解決對象之間循環引用的問題,是以沒有Java虛拟機使用此算法。
- 可作為GC Roots的對象有虛拟機棧中引用的對象、方法區靜态引用的對象、方法區常量引用的對象、本地方法棧中JNI(Native方法)引用的對象
三、分代概念
了解了可達性分析就可以知道哪些對象已死,當GC發生時就需要在整個堆中所有對象開始可達性分析,随着堆的容量變大,每次GC要回收的對象就越多,分析的時間就越長,回收效率将大幅降低,而且很多對象常駐記憶體每次都被分析也會浪費時間。
于是,分代概念出現了,分代将整個堆分為年輕代與老年代,将存活周期短的對象放入年輕代,将存活周期長的對象放入老年代,當記憶體不足時,回收單個代比回收整個堆效率要高。
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLxAzNxEDN4MDNx0yNxcDOyADN5ADOxIDMxIDMy0CO5MTO0ETMvwlMwEjMwIzLchTOzkDNxEzLcd2bsJ2Lc12bj5ycn9Gbi52YuAjMwIzZtl2Lc9CX6MHc0RHaiojIsJye.png)
上圖所示即堆記憶體劃分,其中年輕代、老年代、永久代在Java堆實作,在Oracle JDK1.8後廢棄了永久代的概念,将永久代移動到非堆記憶體中,改名為元空間。綠色表示已廢棄,用作對比。
1、年輕代
年輕代回收頻率高,為了進一步縮小回收範圍,将年輕代又分為:
- Eden:新生對象首先會配置設定到這裡
- Survivor:初生區回收後存活對象轉移到幸存區,S0與S1使用時每次隻用其中一塊,回收時交換。
- S0(From Survior):發生回收的區域,存活對象複制到S1,下次運作時暫不使用
- S1(To Survior):接收S0的存活對象,作為下次運作時使用的區域
當年輕代發生GC後記憶體不足,則将Survivor區存活的對象按一定規則進入老年代,由老年代作年輕代的後備。年輕代記憶體不足還有我老年代嘛~
2、老年代
老年代處于堆中,活得久的對象都在這裡。當年輕代放不下大對象時,将大對象和活得久的對象放到老年代,老年代的特點是回收頻率低、這裡的對象都長壽,沒有其他記憶體對此代做後備。
3、永久代
永久代是 HotSpot 虛拟機中的概念,在堆中實作了方法區,與年輕代和老年代分堆記憶體。由于方法區中存有大量類中繼資料與字面量常量池,類中繼資料解除安裝條件苛刻等原因常駐此代,幾乎不發生GC,永久代由此得名。同時也因為類中繼資料難以解除安裝,當永久代記憶體到達設定值時,容易發生OOM。
在Oracle JDK 1.8之後,将永久代移出了 JVM 堆記憶體,進入直接記憶體并改名為元空間,按直接記憶體回收記憶體的方式管理(不設定元空間大小時,隻受限于剩餘記憶體的餘量;設定元空間最大值時,則在接近滿時,由 JVM 進行一次垃圾回收,不再像堆中回收規則那麼多。可以說是永久代從親娘家搬到了後娘家,不隻改了姓,還變成了後娘養的😆)。
4、元空間
元空間是JDK 1.8後,使用直接記憶體(或稱 堆外記憶體 或 非堆記憶體)實作了方法區,作為永久代的替代品。元空間不再配置設定在堆上,是以年輕代與老年代的可用記憶體會更多,也避免了因永久代 OOM 出現的 Full GC,減少了STW(Stop The World)與 GC(垃圾回收)的頻率,提高了性能。
4.1. 元空間與永久代差別
- 配置設定位置:永久代配置設定在堆上;元空間配置設定在堆外記憶體。
- 回收時刻:永久代與老年代幾乎同時進行GC,回收頻率與;元空間使用量到達設定最大值前才會進行GC,預設此值無限,受實體記憶體限制。
- 設定最大記憶體值:永久代在劃分最大值時就直接占用最大值空間;而元空間則依舊按原來的方式,用一些就申請稍大一些的記憶體,直到到達最大值後不再申請新記憶體。
四、垃圾收集算法
有了分代和對象可達性分析算法還不夠,回收性能高低需要通過合适的垃圾回收算法決定。常見的垃圾回收算法有四種:标記-清除算法、複制算法、标記-整理算法、分代回收算法。
1、标記-清除算法
正如其名,先通過對象判活、标記已死對象,然後再統一回收的算法。
缺點:
- 效率不高
- 标記清除後會産生大量不連續的記憶體碎片,空間使用率低。
2、複制算法
複制算法将一塊記憶體劃分為兩塊,每次隻使用其中一塊,先将存活對象複制到另一塊記憶體中,再回收已死對象,這樣就解決了标記-清除算法回收後空間碎片的問題,實作簡單、運作高效。
适用于年輕代,死亡對象多複制對象少的場景。
- 代價高,需要将記憶體縮小為原來的一半。不适用于老年代,這種對象存活率較高的複制操作效率低。
- 需要其它記憶體作擔保,防止出現GC後Survivor區仍無法配置設定記憶體的情況。
3、标記-整理算法
标記-整理算法算是對标記-清除算法的一個改進,但标記後并不是直接清理,而是将存活對象向一端移動,然後清理掉端邊界以外的記憶體,其實可以視為“标記-移動-清除”。适用于老年代,存活對象多,死亡對象少的場景。
缺點:不适用于年輕代,存活對象少,死亡對象多的場景。
4、分代回收算法
有了分代的概念并了解了以上幾種算法的優缺點,就可以針對不同分代的特點選用适合的GC算法。
- 年輕代對象朝生夕死,死亡對象遠多于存活對象,且有老年代擔保,使用複制算法更合适。
- 老年代對象存活率高,存活對象遠多于死亡對象,沒有記憶體擔保,使用标記-清理或标記-整理更适合。
五、垃圾收集器
垃圾收集器是垃圾回收算法的具體實作,以下是JVM設計團隊給出的各垃圾收集器适用的分代情況
以上圖示,相連的垃圾回收器可配合使用。
1、Serial 收集器
Serial 收集器 是最早的垃圾收集器,使用複制算法,特點是啟動單線程去清理,需暫停所有工作線程,有STW,直到收集完成。用于年輕代,是Client模式下預設的年輕代收集器。
1.1. Serial 執行流程
- 停止使用者線程,STW
- 開啟單線程使用複制算法,複制存活對象到To Survivor區
- 交換使用Survivor區指針
- 清理From Survivor區垃圾
- 恢複使用者線程
1.2. VM 參數
-
:使用 Serial 收集器,Client模式預設開啟,其他模式關閉,打開此開關預設同時開啟 Serial + Serial Old 收集器配合回收堆記憶體。-XX:+UseSerialGC
1.3. 舉個例子
你媽(單線程)打掃衛生時,她會讓你不要動(STW),把你弄亂的東西搬到不礙事(S1)的地方擺好(複制),接着她告訴你一會先去那個收拾好的地方玩(S1),然後開始掃地(清理S0)。掃完她退出房間,你又可以玩耍了 😄
2、ParNew 收集器
ParNew 收集器 是對 Serial 收集器的多線程版本,使用複制算法,特點是暫停所有工作線程,STW,開啟多個GC線程并行垃圾收集,直到收集完成。用于年輕代,是Server模式下首選的收集器。原因是除Serial外,隻有它能與CMS收集器配合工作。
2.1. ParNew 執行流程
- 開啟多線程并行将存活對象複制到S0(To Survivor)區
- 交換使用Survivor區指針(原來使用S0換成S1,反之亦然)
- 并行清理S1(From Survivor)區垃圾
2.2. VM 參數
-
:預設關閉,打開後使用ParNew + Serial Old收集器組合進行記憶體回收。-XX:+UseParNewGC
2.3. 舉個例子
一家人(多線程)沖進你的房間,警告你不要動(STW),他們要一起打掃衛生(并行),把除垃圾外的東西移動到另一半房間中(複制存活對象到S1),然後再一起掃地髒的半個房間,完事後才讓你動,并且隻能去另一半還沒使用的房間玩……
3、Parallel Scavenge 收集器
Parallel Scavenge 收集器與ParNew 收集器很類似,也是多線程并行、年輕代收集器,使用複制算法,用在 Server 模式下。與 ParNew 的差別在于,Parallel Scavenge更關注吞吐量,能實作自适應GC調優。
3.1. Parallel Scavenge 執行流程
- 開啟多線程,并行複制存活對象到 S1 區
- 交換使用 Survivor 區指針
- 并行清理 S0 區垃圾
清理過程中PS收集器會計算停頓時間與吞吐量,停頓時間與吞吐量即将不滿足前會提前停止垃圾收集,恢複使用者線程。
3.2. VM 參數
-
:Server模式預設開啟,其他模式預設關閉,打開後使用Parallel Scavenge + Serial Old 收集器組合記憶體回收。-XX:+UseParallelGC
-
: 控制最大垃圾收集停頓時間,機關毫秒,僅在使用Parallel Scavenge 收集器時生效。-XX:MaxGCPauseMillis
-
: 直接控制吞吐量大小,預設值99,表示允許1%的時間用來GC,僅在Parallel Scavenge 收集器時生效。-XX:GCTimeRatio
-
:自适應GC調優開關,預設開啟,使用此參數則無需指定年輕代大小(-XX:+UseAdaptiveSizePolicy
)、Eden 與 Survivor 區的比例(-Xmn
)、晉升老年代對象大小(-XX:SurvivorRatio
)等參數,由虛拟機自動設定。-XX:PretenureSizeThreshold
3.3. 舉個例子
一家人(多線程)沖進你的房間要打掃衛生,你和他們約定好“隻給他們3分鐘時間打掃衛生”(制定停頓時間或吞吐量要求),打掃衛生的時候你不會動(STW),就算時間到了沒清理完,也别打掃了你還要學習呢(時間片用完退出GC,恢複使用者線程)。于是可能出現他們隔一會進來打掃3分鐘,過了一會又進來打掃了3分鐘……由于打掃時間不是很長,你感覺還可以接受 🤔
4、Serial Old 收集器
Serial Old 收集器是Serial 收集器的老年代版本,也是單線程垃圾收集,使用标記-整理算法,主要用在Client模式下,是CMS收集器出現
Concurrent Mode Failure
錯誤時的後備方案。可與 Parallel Scavenge收集器配合使用。
4.1. Serial Old 執行流程
- 開啟單線程使用标記-整理清理垃圾
- 清理完成,恢複使用者線程
4.2. VM 參數
由
-XX:+UseSerialGC
或 開啟CMS時作為備用方案連帶使用。
4.3. 舉個例子
你媽(單線程)過來掃地,先讓你不要動(STW),這次媽媽看地上東西比較多(老年代存活對象多),決定不搬到桌子上了,隻把地上有用的東西搬到地面的一個幹淨的角落(标記-整理),然後把其餘的地方都掃了一遍(清理整理好的對象邊界外的記憶體),然後她出了門,你又可以玩耍了(退出GC線程,恢複使用者線程)。
5、Parallel Old 收集器
Parallel Old 收集器是Parallel Scavenge 收集器的老年代版本,使用多線程并行和标記-整理算法。注重吞吐量和CPU敏感的場合,優先考慮Parallel Scavenge與Parallel Old收集器配合使用。
5.1. Parallel Old 執行流程
- 開啟多線程并行使用标記-整理清理垃圾
5.2. VM 參數
-
:預設關閉,開啟後使用Parallel Scavenge + Parallel Old 收集器組合記憶體回收。-XX:+UseParallelOldGC
5.3. 舉個例子
一家人(多線程)又沖進來幫忙打掃衛生,警告你不要動(STW),你說“我不動可以,但你們要3分鐘後出去下,我要和女朋友打電話”(制定吞吐量或停頓時間計劃),家人們露出會心的微笑并同意了你的建議,他們定睛一看屋子裡垃圾不多,能用的東西很多但是亂(老年代存活對象多,垃圾少),為了省事把那些能用的東西一起(并行)搬到一個角落(标記-整理),然後把垃圾清理幹淨,讓你從桌子上下來(恢複使用者線程),出了門 😆
6、CMS 收集器
CMS(Concurrent Mark Sweep) 收集器目标是最短GC的停頓時間,特點是并發收集、GC停頓時間短,使用标記-清除算法。網際網路應用與B/S系統服務端使用較多。
6.1. CMS 執行流程
此收集器工作流程相對複雜,大體分四個步驟:
- 初始标記(initial mark):STW停止工作線程,單标記線程标記與GCRoots直接關聯的對象,标記完成工作線程繼續執行。
- 并發标記(concurrent mark):标記與GCRoots間接關聯的對象,并發标記不停止工作線程,标記線程與工作線程同時運作。
- 重新标記(remark):STW停止工作線程,開啟多線程标記 并發标記 後工作線程産生的新垃圾,标記完成恢複工作線程運作。
- 并發清除(concurrent sweep):開始清理線程與工作線程并發執行。
工作流程步驟執行時間比較:
并發清理 = 并發标記 > 重新标記 > 初始标記
筆者注:并發清理與并發标記時間不一定相等,兩者都比較慢可視為約等于,沒必要較真。
6.2. CMS收集器缺點
- 使用标記-清除算法,會出現大量不連續的記憶體碎片而導緻Full GC。當出現
,則需要Serial Old收集器墊底,可能出現新的Full GC。Concurrent Mode Failure
- CMS收集器對CPU敏感,當CPU數量少于4個時,對應用程式性能影響較大。
- 無法處理浮動垃圾,主要是并發清理時工作線程産生的垃圾需要下次GC時才能标記清理掉。
6.3. VM 參數
-
:預設關閉,使用ParNew + CMS + Serial Old組合記憶體回收,如果CMS收集器出現-XX:+UseConcMarkSweepGC
,則使用 Serial Old收集器作後備收集器。Concurrent Mode Failure
-
:提高CMS啟動門檻值(老年代已用記憶體占比),降低回收頻次。-XX:CMSInitiatingOccupancyFraction
-
:開啟此參數,CMS将在即将Full GC前進行記憶體碎片的合并。-XX:+UseCMSCompactAtFullCollection
-
:設定執行多少次不整理碎片的Full GC後來一次帶整理的,預設值為0,即每次Full GC時都進行碎片整理。-XX:CMSFullGCsBeforeCompaction
6.4. 舉個例子
- 一開始,你媽(單線程)進來看看大體上哪些東西(GC Roots直接關聯)需要要收拾,記到本子上(初始标記),但不讓你動(STW)
- 你站累了,央求你媽,她讓你下來玩,她接着仔細看還有哪些東西需要收拾記在小本子上(并發标記,使用者線程仍在執行)
- 你下來後弄亂了點其它東西,你媽讓你停下來(STW),把家人也叫過來幫忙看看本子上沒有的新弄亂的東西(重新标記,記錄新增的垃圾)
- 你媽熬不過你的央求,放你下來了(使用者線程恢複運作),按着小本子上記的該收拾的東西移到一個幹淨的角落擺整齊(标記-整理),因為你也要動,會妨礙到她,她動作很慢(并發執行與使用者線程搶時間片),這次清理完時,隻是把她最後一次記錄的東西收拾完了,你在她記錄完後弄亂的東西隻能她下次再來收拾。(CMS無法清理浮動垃圾)
7、G1 收集器
G1(Garbage First)是面向服務端應用的GC收集器,保留分代概念的同時,将堆分成多塊同樣大小的記憶體塊Region,回收整個堆的記憶體,關注停頓時間。
7.1. G1 的特點
- 有價值的垃圾優先回收(最短時間内釋放最多的記憶體的記憶體塊)
- 回收範圍是整個堆,保留年輕代與老年代的概念,将整個堆分成多個大小相等的獨立區域(Region),年輕代與老年代實體不再隔離。
- 充分發揮多核CPU優勢,采用并行與并發縮短STW時間。
- 沒有空間碎片。整體可視作使用标記-整理算法,局部(兩個Region間複制)可看作是複制算法,兩種算法不會産生碎片。
- 可以準确設定在M毫秒内,停頓時間不得超過N毫秒。
7.2. G1 為什麼能縮短停頓時間?
G1縮短停頓時間的秘決,是建立在通過 維護 Region垃圾價值 優先回收清單上的,根據允許的收集時間優先回收價值最大的Region,同時記錄各Region中建立對象的引用關系集合(Remembered Set)來避免全堆掃描,以提高引用可達性分析與标記的時間。
7.3. G1 執行流程
G1收集器大緻回收步驟:
- 初始标記:單線程标記GC Roots直接關聯的對象,并且修改 TAMS(Next Top at Mark Start)的值,讓下一階段與使用者程式并發執行時,能在可用的Region中建立新對象,此階段需要STW,耗時很短。
- 并發标記:與使用者線程并發執行标記,從GC Roots開始進行可達性分析,标記存活對象,此階段耗時較長。
- 最終标記:停頓使用者線程,STW,多線程并行标記修正 并發标記 階段使用者線程運作産生的新垃圾。同時将記錄資訊合并到 Remembered Set 中,用作篩選回收的依據。
- 篩選回收:停頓使用者線程,STW,根據Remembered Set 對各個Region回收價值進行排序,根據使用者設定的停頓時間,制定回收計劃,并多線程并行回收最有價值的Region。回收完成恢複使用者線程。
G1 與 PS/PS Old相比,最大的好處是停頓時間更加可控,可預測。
7.4. VM 參數
-
:預設關閉,使用G1收集器回收堆記憶體。-XX:+UseG1GC
7.5. 舉個例子
- 房間在沒住進來前,地面就已經按大理石地磚分成了好多等塊的位置(Region)
- 你媽進來,讓你别動(STW),大體上看看屋子哪些東西一眼就看得出亂,小本本記下來(初始标記,直接關聯GC Roots),你和她約法三章,一會要寫作業。(設定停頓時間)
- 她說你可以動了(恢複使用者線程),你在各個地磚上玩,她在其他地磚上記哪些地磚上垃圾比較多,由于你也在動,她動作很慢,怕傷到你。(并發執行)
- 時間久了,你媽也累了,不讓你動了(STW),叫家人進來一起幫忙記(多線程并行,最終标記)。
- 最後,家人一起把地磚上相鄰的有用的東西放在一個地磚上,清理原來的地磚(相鄰Region複制),最後把這些有用的東西移到一個角落擺整齊(整體看作标記-整理),看約定的時間到了,退出了你的房間(GC停頓時間到達,退出GC程序,恢複使用者線程)。
7.6. 是否該選用 G1
如果目前使用的收集器滿足需要,就不必使用 G1;如果應用追求低停頓,可以嘗試 G1;如果你的應用追求吞吐量,那麼使用PS/PS Old 收集器組合想必會更适合,G1對吞吐量沒有什麼提升。
六、記憶體溢出是什麼?
記憶體溢出在Java中用
OutOfMemeryError
表示,當堆記憶體中無法為新生對象申請記憶體後,GC(垃圾回收)無效時,則報出記憶體溢出錯誤,簡稱 OOM 或 OOME。
1、OOM 出現的原因
- 老年代記憶體不足:java.lang.OutOfMemoryError:Javaheapspace
- 永久代記憶體不足:java.lang.OutOfMemoryError:PermGenspace (JDK 1.7及以前)
- 元空間記憶體設定較小:java.lang.OutOfMemoryError: Metaspace
- 代碼bug,占用記憶體無法及時回收
2、出現OOM簡單處理
- 如服務已設定
及-XX:+HeapDumpOnOutMemoryError
訓示出現OOM時進行生成堆Dump(堆快照檔案),取堆Dump檔案分析問題改正問題,重新部署新程式-Xloggc
- 如未設定,設定堆溢出生成Dump參數,重新開機服務,伺機問題複現時收集并分析堆Dump
- 設定堆記憶體最小值
和最大值-Xms
,最大值參考曆史使用率設定-Xmx
- 設定GC垃圾收集器為G1
- 啟用GC日志
,友善後期分析-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
- 如果是元空間記憶體溢出,調大
的值,如-XX:MaxMetaspaceSize
-XX:MaxMetaspaceSize=100m
七、術語參考
-
(簡稱GC,Garbage Collection):垃圾收集,清理JVM中無用的對象,釋放其占用的記憶體。垃圾收集
-
:垃圾收集過程中會出現Stop The World(STW)即停頓使用者線程執行清理,清理完成後恢複使用者線程繼續運作。随着垃圾收集器越來越優秀,使用者線程停頓的時間會越來越短,但仍未完全消除。STW
-
(Safepoint):代碼執行過程中特殊的位置,程式可暫停的點。發生GC時,需要使用者線程跑到最近的安全點上才可以開始清理。安全點
- GC時使用者線程未到達安全點解決方案:
- 搶先式中斷 :發現需要中斷時,線程未在安全點上,則讓線程跑到最近安全點再中斷。幾乎沒有虛拟機使用此方案。
- 主動式中斷:當GC需要暫停線程時,在安全點處設定中斷标記,使用者線程主動去軟體輪詢這個标志,如果有中斷标記即中斷線程執行。
-
:指在一段代碼片段中,引用關系不會發生變化,在該區域的任何地方發生gc都是安全的。為了解決使用者線程準備GC時不在安全點的問題。安全區域
-
:指的是多線程收集,但工作線程仍處于暫停狀态。并行收集
-
:工作線程與垃圾收集線程同時執行。并發收集
-
(Throughput):即工作線程執行的時間占程式總運作時間的比值。即吞吐量
。吞吐量=運作使用者代碼時間 / (運作使用者代碼時間 + 垃圾收集時間)
-
:老年代GC,又名 Major GC。Full GC
-
:年輕代GC。Minor GC
八、如何選擇GC收集器:
- 如果堆比較小,隻有大約100m左右,使用
-XX:+UseSerialGC
- 如果應用運作在單核心CPU上,并且對停頓時間沒要求,使用
XX:+UseSerialGC
- 追求最巅峰的性能(高吞吐量),并且對停頓時間要求不多或能滿足要求時,使用
-XX:+UseParallelGC
- 響應時間比總吞吐量更重要,垃圾收集暫停必須保持最短時,使用
-XX:+UseG1GC
- 響應時間優先級高,且有巨大的堆,考慮更新jdk到11,并使用
(OracleJDK)或-XX:+UseZGC
(OpenJDK)-XX:+UseShenandoahGC
- 以上判斷都建立在應用程式代碼對新版本JDK的相容性上,如果是舊版本項目依賴舊版本JDK,堆大概在6G及以下,使用CMS或許會更好,如果堆更大,可嘗試G1。
總結
了解JVM虛拟機垃圾回收機制與垃圾收集器的實作适用場景,有助于解決線上出現Java程式記憶體溢出的問題。
本文系參考了《深入了解Java虛拟機 第2版》第3章内容整理的學習筆記,侵權删。
最後,由于本人水準有限,行文中難免出現錯誤,希望看官可以評論指出,感激不盡!對于各版本JDK的JVM調優參數會略有不同,請以使用版本文檔為準!
參考
- 《深入了解Java虛拟機 第2版》周志明著
- https://blogs.oracle.com/jonthecollector/our-collectors
- https://blog.51cto.com/lizhenliang/2164876
- https://docs.oracle.com/en/java/javase/14/gctuning/available-collectors.html#GUID-9E4A6B11-BB94-424F-90EF-401287A1C333
本文同步于本人CSDN