JVM垃圾回收機制
1. 概述
垃圾回收(Garbage Collection,GC),顧名思義就是釋放垃圾佔用的空間,防止內存泄露。有效的使用可以使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。
2. 垃圾判斷算法
2.1 引用計數法
給每個對象添加一個計數器,當有地方引用該對象時計數器加1,當引用失效時計數器減1。用對象計數器是否爲0來判斷對象是否可被回收。缺點:無法解決循環引用的問題。
先創建一個字符串,String m = new String(“jack”);,這時候 “jack” 有一個引用,就是m。然後將m設置爲null,這時候 “jack” 的引用次數就等於 0 了,在引用計數算法中,意味着這塊內容就需要被回收了。
引用計數算法是將垃圾回收分攤到整個應用程序的運行當中了,而不是在進行垃圾收集時,要掛起整個應用的運行,直到對堆中所有對象的處理都結束。因此,採用引用計數的垃圾收集不屬於嚴格意義上的Stop-The-World的垃圾收集機制。
看似很美好,但我們知道JVM的垃圾回收就是Stop-The-World的,那是什麼原因導致我們最終放棄了引用計數算法呢?看下面的例子。
public class ReferenceCountingGC {
public Object instance;
public ReferenceCountingGC(String name) {
}
public static void testGC(){
ReferenceCountingGC a = new ReferenceCountingGC("objA");
ReferenceCountingGC b = new ReferenceCountingGC("objB");
a.instance = b;
b.instance = a;
a = null;
b = null;
}
}
我們可以看到,最後這2個對象已經不可能再被訪問了,但由於他們相互引用着對方,導致它們的引用計數永遠都不會爲0,通過引用計數算法,也就永遠無法通知GC收集器回收它們。
2.2 可達性分析算法
通過GC ROOT的對象作爲搜索起始點,通過引用向下搜索,所走過的路徑稱爲引用鏈。通過對象是否有到達引用鏈的路徑來判斷對象是否可被回收(可作爲GC ROOT的對象:虛擬機棧中引用的對象,方法區中類靜態屬性引用的對象,方法區中常量引用的對象,本地方法棧中JNI引用的對象
)
通過可達性算法,成功解決了引用計數所無法解決的循環依賴問題,只要你無法與GC Root建立直接或間接的連接,系統就會判定你爲可回收對象。那這樣就引申出了另一個問題,哪些屬於GC Root。
Java內存區域中可以作爲GC ROOT的對象:
- 虛擬機棧中引用的對象
public class StackLocalParameter {
public StackLocalParameter(String name) {}
public static void testGC() {
StackLocalParameter s = new StackLocalParameter("localParameter");
s = null;
}
}
此時的s,即爲GC Root,當s置空時,localParameter對象也斷掉了與GC Root的引用鏈,將被回收。
- 方法區中類靜態屬性引用的對象
public class MethodAreaStaicProperties {
public static MethodAreaStaicProperties m;
public MethodAreaStaicProperties(String name) {}
public static void testGC(){
MethodAreaStaicProperties s = new MethodAreaStaicProperties("properties");
s.m = new MethodAreaStaicProperties("parameter");
s = null;
}
}
此時的s,即爲GC Root,s置爲null,經過GC後,s所指向的properties對象由於無法與GC Root建立關係被回收。而m作爲類的靜態屬性,也屬於GC Root,parameter 對象依然與GC root建立着連接,所以此時parameter對象並不會被回收。
- 方法區中常量引用的對象
public class MethodAreaStaicProperties {
public static final MethodAreaStaicProperties m = MethodAreaStaicProperties("final");
public MethodAreaStaicProperties(String name) {}
public static void testGC() {
MethodAreaStaicProperties s = new MethodAreaStaicProperties("staticProperties");
s = null;
}
}
m即爲方法區中的常量引用,也爲GC Root,s置爲null後,final對象也不會因沒有與GC Root建立聯繫而被回收。
- 本地方法棧中引用的對象
任何native接口都會使用某種本地方法棧,實現的本地方法接口是使用C連接模型的話,那麼它的本地方法棧就是C棧。當線程調用Java方法時,虛擬機會創建一個新的棧幀並壓入Java棧。然而當它調用的是本地方法時,虛擬機會保持Java棧不變,不再在線程的Java棧中壓入新的幀,虛擬機只是簡單地動態連接並直接調用指定的本地方法。
3. 垃圾回收算法
在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裏面涉及到一個問題是:如何高效地進行垃圾回收。這裏我們討論幾種常見的垃圾收集算法的核心思想。
3.1 標記-清除算法
特徵:在空間中存活對象較多的情況下較爲高效,但由於標記-清除採用的爲直接回收不存活對象所佔用的內存,因此會造成內存碎片。
標記清除算法(Mark-Sweep)是最基礎的一種垃圾回收算法,它分爲2部分,先把內存區域中的這些對象進行標記,哪些屬於可回收標記出來,然後把這些垃圾拎出來清理掉。就像上圖一樣,清理掉的垃圾就變成未使用的內存區域,等待被再次使用。但它存在一個很大的問題,那就是內存碎片。
上圖中等方塊的假設是2M,小一些的是1M,大一些的是4M。等我們回收完,內存就會切成了很多段。我們知道開闢內存空間時,需要的是連續的內存區域,這時候我們需要一個2M的內存區域,其中有2個1M是沒法用的。這樣就導致,其實我們本身還有這麼多的內存的,但卻用不了。
3.2 複製算法
特徵:當要回收的空間中存活對象較少時,複製算法會比較高效,其帶來的成本是要增加一塊空的內存空間及進行對象的移動。
複製算法(Copying)是在標記清除算法基礎上演化而來,解決標記清除算法的內存碎片問題。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。保證了內存的連續可用,內存分配時也就不用考慮內存碎片等複雜情況。複製算法暴露了另一個問題,例如硬盤本來有500G,但卻只能用200G,代價實在太高。
3.3 標記-整理算法
特徵:在標記-清除的基礎上還須進行對象的移動,成本相對更高,好處則是不產生內存碎片。
標記-整理算法標記過程仍然與標記-清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,再清理掉端邊界以外的內存區域。
標記整理算法解決了內存碎片的問題,也規避了複製算法只能利用一半內存區域的弊端。標記整理算法對內存變動更頻繁,需要整理所有存活對象的引用地址,在效率上比複製算法要差很多。
3.4 分代收集算法
分代收集算法分代收集算法嚴格來說並不是一種算法,而是融合上述3種基礎的算法思想,而產生的針對不同情況所採用不同算法的一套組合拳,根據對象存活週期的不同將內存劃分爲幾塊。
-
在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。
-
在老年代中,因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清理算法或者標記-整理算法來進行回收。
4.垃圾收集器
如果說垃圾回收算法是內存回收的方法論,那麼垃圾收集器就是具體實現。jvm會結合針對不同的場景及用戶的配置使用不同的收集器。
- 年輕代收集器
Serial、ParNew、Parallel Scavenge - 老年代收集器
Serial Old、Parallel Old、CMS收集器 - 特殊收集器
G1收集器[新型,不在年輕、老年代範疇內]
新生代收集器
Serial收集器
Serial(串行)收集器收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單線程收集器了。它的 “單線程” 的意義不僅僅意味着它只會使用一條垃圾收集線程去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作線程( “Stop The World” ),直到它收集結束。
新生代採用複製算法,老年代採用標記-整理算法。
虛擬機的設計者們當然知道Stop The World帶來的不良用戶體驗,所以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。
但是Serial收集器有沒有優於其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單線程相比)。Serial收集器由於沒有線程交互的開銷,自然可以獲得很高的單線程收集效率。Serial收集器對於運行在Client模式下的虛擬機來說是個不錯的選擇。
ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行爲(控制參數、收集算法、回收策略等等)和Serial收集器完全一樣。
新生代採用複製算法,老年代採用標記-整理算法。
它是許多運行在Server模式下的虛擬機的首要選擇,除了Serial收集器外,只有它能與CMS收集器(真正意義上的併發收集器,後面會介紹到)配合工作。
Parallel Scavenge收集器
Parallel Scavenge 收集器類似於ParNew 收集器。 那麼它有什麼特別之處呢?
-XX:+UseParallelGC
使用Parallel收集器+ 老年代串行
-XX:+UseParallelOldGC
使用Parallel收集器+ 老年代並行
Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是用戶線程的停頓時間(提高用戶體驗)。所謂吞吐量就是CPU中用於運行用戶代碼的時間與CPU總消耗時間的比值。 Parallel Scavenge收集器提供了很多參數供用戶找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解的話,手工優化存在的話可以選擇把內存管理優化交給虛擬機去完成也是一個不錯的選擇。
新生代採用複製算法,老年代採用標記-整理算法。
老年代收集器
Serial Old收集器
Serial收集器的老年代版本,它同樣是一個單線程收集器。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作爲CMS收集器的後備方案。
Parallel Old收集器
**Parallel Scavenge收集器的老年代版本。**使用多線程和“標記-整理”算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它而非常符合在注重用戶體驗的應用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作。
從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種 “標記-清除”算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分爲四個步驟:
- 初始標記: 暫停所有的其他線程,並記錄下直接與root相連的對象,速度很快 ;
- 併發標記: 同時開啓GC和用戶線程,用一個閉包結構去記錄可達對象。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達對象。因爲用戶線程可能會不斷的更新引用域,所以GC線程無法保證可達性分析的實時性。所以這個算法裏會跟蹤記錄這些發生引用更新的地方。
- 重新標記: 重新標記階段就是爲了修正併發標記期間因爲用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
- 併發清除: 開啓用戶線程,同時GC線程開始對爲標記的區域做清掃。
從它的名字就可以看出它是一款優秀的垃圾收集器,主要優點:併發收集、低停頓。但是它有下面三個明顯的缺點:
- 對CPU資源敏感;
- 無法處理浮動垃圾;
- 它使用的回收算法-“標記-清除”算法會導致收集結束時會有大量空間碎片產生。
特殊收集器
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收集器的運作大致分爲以下幾個步驟:
- 初始標記
- 併發標記
- 最終標記
- 篩選回收
G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內可以儘可能高的收集效率(把內存化整爲零)。
5. Java GC機制
Minor GC
在年輕代Young space(包括Eden區和Survivor區)中的垃圾回收稱之爲 Minor GC,Minor GC只會清理年輕代.
Major GC
Major GC清理老年代(old GC),但是通常也可以指和Full GC是等價,因爲收集老年代的時候往往也會伴隨着升級年輕代,收集整個Java堆。所以有人問的時候需問清楚它指的是full GC還是old GC。
Full GC
full gc是對新生代、老年代、永久代【jdk1.8後沒有這個概念了】統一的回收。
【知乎R大的回答:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)、元空間(1.8及以上)等所有部分的模式】
mixed GC【g1特有】
混合GC
收集整個young gen以及部分old gen的GC。只有G1有這個模式
參考
https://www.jianshu.com/p/23f8249886c6
https://blog.csdn.net/qq_34337272/article/details/82177383