深入理解JVM虛擬機3:垃圾回收器詳解

本文轉自:https://www.cnblogs.com/snailclimb/p/9086341.html

本系列文章將整理到我在GitHub上的《Java面試指南》倉庫,更多精彩內容請到我的倉庫裏查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章將同步到我的個人博客:

www.how2playlife.com

<!-- more -->

本節常見面試題(推薦帶着問題閱讀,問題答案在文中都有提到):

如何判斷對象是否死亡(兩種方法)。

簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。

垃圾收集有哪些算法,各自的特點?

HotSpot爲什麼要分爲新生代和老年代?

常見的垃圾回收器有那些?

介紹一下CMS,G1收集器。

Minor Gc和Full GC 有什麼不同呢?

1 概述

首先所需要考慮:

  • 那些垃圾需要回收?
  • 什麼時候回收?
  • 如何回收?

當需要排查各種 內存溢出問題、當垃圾收集稱爲系統達到更高併發的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。

2 對象已經死亡?

堆中幾乎放着所有的對象實例,對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象)

2.1引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任何時候計數器爲0的對象就是不可能再被使用的。

這個方法實現簡單,效率高,但是目前主流的虛擬機中並沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。

2.2可達性分析算法

這個算法的基本思想就是通過一系列的稱爲 “GC Roots” 的對象作爲起點,從這些節點開始向下搜索,節點所走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連的話,則證明此對象是不可用的。
可達性分析算法

2.3 再談引用

JDK1.2以後,Java對引用的感念進行了擴充,將引用分爲強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

1.強引用

以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似於必不可少的生活用品,垃圾回收器絕不會回收它。當內存空 間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。

2.軟引用(SoftReference)

如果一個對象只具有軟引用,那就類似於可有可物的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。

軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。

3.弱引用(WeakReference)

如果一個對象只具有弱引用,那就類似於可有可物的生活用品。弱引用與軟引用的區別在於:只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由於垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。

4.虛引用(PhantomReference)

“虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定對象的生命週期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

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

虛引用與軟引用和弱引用的一個區別在於: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的內存被回收之前採取必要的行動。

特別注意,在程序設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因爲軟引用可以加速JVM對垃圾內存的回收速度,可以維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生

2.4 生存還是死亡

即使在可達性分析法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,要真正宣告一個對象死亡,至少要經歷兩次標記過程;可達性分析法中不可達的對象被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize方法。當對象沒有覆蓋finalize方法,或finalize方法已經被虛擬機調用過時,虛擬機將這兩種情況視爲沒有必要執行。被判定爲需要執行的對象將會被放在一個隊列中進行第二次標記,除非這個對象與引用鏈上的任何一個對象建立關聯,否則就會被真的回收。

2.5 回收方法區

方法區(或Hotspot虛擬中的永久代)的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類” :

  • 該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。
  • 加載該類的ClassLoader已經被回收。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

3 垃圾收集算法

3.1 標記-清除算法

算法分爲“標記”和“清除”階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。它是最基礎的收集算法,會帶來兩個明顯的問題;1:效率問題和2:空間問題(標記清除後會產生大量不連續的碎片)
標記-清除算法

3.2 複製算法

爲了解決效率問題,“複製”收集算法出現了。它可以將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象複製到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。

複製算法

3.3 標記-整理算法

根據老年代的特點特出的一種標記算法,標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一段移動,然後直接清理掉端邊界以外的內存。

標記-整理算法

3.4分代收集算法

當前虛擬機的垃圾手機都採用分代收集算法,這種算法沒有什麼新的思想,只是根據對象存活週期的不同將內存分爲幾塊。一般將java堆分爲新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。

比如在新生代中,每次收集都會有大量對象死去,所以可以選擇複製算法,只需要付出少量對象的複製成本就可以完成每次垃圾收集。而老年代的對象存活機率是比較高的所以我們可以選擇“標記-清理”或“標記-整理”算法進行垃圾收集。

延伸面試問題: HotSpot爲什麼要分爲新生代和老年代?

根據上面的對分代收集算法的介紹回答。

4 垃圾收集器

如果說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。
雖然我們對各個收集器進行比較,但並非了挑選出一個最好的收集器。因爲知道現在位置還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那麼我們的HotSpot虛擬機就不會實現那麼多不同的垃圾收集器了。

4.1 Serial收集器

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

虛擬機的設計者們當然知道Stop The World帶來的不良用戶體驗,所以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是Serial收集器有沒有優於其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial收集器由於沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial收集器對於運行在Client模式下的虛擬機來說是個不錯的選擇。

4.2 ParNew收集器

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

ParNew收集器
它是許多運行在Server模式下的虛擬機的首要選擇,除了Serial收集器外,只有它能與CMS收集器(真正意義上的併發收集器,後面會介紹到)配合工作。

並行和併發概念補充:

  • 並行(Parallel) :指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態。

  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是並行,可能會交替執行),用戶程序在繼續運行,而垃圾收集器運行在另一個CPU上。

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的的多線程收集器。。。那麼它有什麼特別之處呢?

Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用於運行用戶代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解的話,手工優化存在的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。

4.4.Serial Old收集器

Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作爲CMS收集器的後備方案。

4.5 Parallel Old收集器

Parallel Scavenge收集器的老年代版本。使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。

4.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它而非常符合在注重用戶體驗的應用上使用。

從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分爲四個步驟:

  • 初始標記: 暫停所有的其他線程,並記錄下直接與root相連的對象,速度很快 ;
  • 併發標記: 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。
  • 重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
  • 併發清除: 開啓用戶線程,同時GC線程開始對爲標記的區域做清掃。

CMS收集器

從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:
對CPU資源敏感;
無法處理浮動垃圾;
它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。

4.7 G1收集器

上一代的垃圾收集器(串行serial, 並行parallel, 以及CMS)都把堆內存劃分爲固定大小的三個部分: 年輕代(young generation), 年老代(old generation), 以及持久代(permanent generation).

年輕代(young generation), 年老代(old generation), 以及持久代(permanent generation)

G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特徵.

被視爲JDK1.7中HotSpot虛擬機的一個重要進化特徵。它具備一下特點:
並行與併發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行。
分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
空間整合:與CMS的“標記–清理”算法不同,G1從整體來看是基於“標記整理”算法實現的收集器;從局部上來看是基於“複製”算法實現的。
可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內。

G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率(把內存化整爲零)。

G1收集器的運作大致分爲以下幾個步驟:
初始標記
併發標記
最終標記
篩選回收

上面幾個步驟的運作過程和CMS有很多相似之處。初始標記階段僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS的值,讓下一個階段用戶程序併發運行時,能在正確可用的Region中創建新對象,這一階段需要停頓線程,但是耗時很短,併發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段時耗時較長,但可與用戶程序併發執行。而最終標記階段則是爲了修正在併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remenbered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set Logs裏面,最終標記階段需要把Remembered Set Logs的數據合併到Remembered Set中,這一階段需要停頓線程,但是可並行執行。最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。

G1收集器

5 內存分配與回收策略

5.1對象優先在Eden區分配

大多數情況下,對象在新生代中Eden區分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC.

Minor Gc和Full GC 有什麼不同呢?

新生代GC(Minor GC):指發生新生代的的垃圾收集動作,Minor GC非常頻繁,回收速度一般也比較快。

老年代GC(Major GC/Full GC):指發生在老年代的GC,出現了Major GC經常會伴隨至少一次的Minor GC(並非絕對),Major GC的速度一般會比Minor GC的慢10倍以上。

5.2 大對象直接進入老年代

大對象就是需要大量連續內存空間的對象(比如:字符串、數組)。

5.3長期存活的對象將進入老年代

既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別那些對象應放在新生代,那些對象應放在老年代中。爲了做到這一點,虛擬機給每個對象一個對象年齡(Age)計數器。

5.4 動態對象年齡判定

爲了更好的適應不同程序的內存情況,虛擬機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果Survivor 空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需達到要求的年齡。

總結:

本節介紹了垃圾收集算法,幾款JDK1.7中提供的垃圾收集器特點以及運作原理。
內存回收與垃圾收集器在很多時候都是影響系統性能、併發能力的主要因素之一,虛擬機之所以提供多種不同的收集器以及大量調節參數,是因爲只有根據實際應用的需求、實現方式選擇最優的收集方式才能獲取最高的性能。沒有固定收集器、參數組合、也沒有最優的調優方法,那麼必須瞭解每一個具體收集器的行爲、優勢和劣勢、調節參數。

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