詳細的講一遍Java GC

本文從Java的垃圾定義、如何回收垃圾、垃圾回收策略等方面詳細講一遍Java的垃圾回收機制

什麼是垃圾回收

垃圾回收(Garbage Collection,GC),就是釋放垃圾佔用的存儲空間,對內存中(主要是堆)已經死亡或長時間未被使用的對象進行清除和回收,防止內存泄漏。

內存模型

先看下jdk 1.6、1.7、1.8的內存模型

jdk1.6

jdk1.7

jdk1.8

可以看到,jdk從1.6到1.8的演進中,將常量池放到了堆裏面,將永久代移出了JVM內存,放到了機器內存中(元空間-metaspace)。初衷在於將不可回收或很難回收的放到了堆外內存(永久代的回收有非常嚴格的要求),可控的收攏在堆內。

如何定義垃圾

GC前,我們得直到哪些內存是可以回收的。這裏就涉及到垃圾回收的相關算法:引用計數算法和可達性分析算法

引用計數法

引用計數法是在對象中分配一塊空間來記錄該對象被引用的次數。如果該對象被其他對象引用一次則+1,如果刪除對該對象的引用,則引用次數-1。當該對象的引用次數爲0時,那麼該對象就可被回收。

String str = new String("west");
str = null;

創建一個字符串,則字符串"west"有1個引用,即 str;將 str 置爲 null,則 "west" 的引用次數爲0。在引用計數算法中,"west"所佔用的內存即可被回收了(寫代碼中,不用的對象顯示置爲null即是這個道理)。

從上面也可以看到,引用計數法是將垃圾判斷邏輯放到了應用的運行當中,而不是在垃圾回收時,停止整個應用的運行(stop the world),直到對堆中的垃圾回收處理完畢。但引用計數法被淘汰也有它的弊端,當我們定義2個對象(計數+1),且這兩個對象相互引用(計數+1),再置空2個對象的聲明引用(計數-1)時,實際這2個對象已經不可能在被訪問了,但因爲計數器不爲0(1+1-1=1),GC回收器永遠無法回收他們。

可達性分析算法

可達性分析算法是通過 GC ROOT 作爲起點,從這些節點向下搜索的路徑,當一個對象在這個路徑上都沒有被任何引用時,則證明該對象是未被使用(不可達)可回收的。

可以看到,可達性算法解決了引用計數算法裏的“循環依賴”問題,只要對象和GC ROOT沒有關聯(直接或間接),則可被回收。

上文說的不用的對象顯示置爲 null 即沒有被引用,即可被及時清理。

什麼是GC ROOT

看上面內存模型圖,GC ROOT 的對象包含:虛擬機棧中引用的對象、方法區中靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中的Native方法引用的對象。

如何回收垃圾

在確定了哪些是垃圾可以回收後,GC要做的就是如何回收這些垃圾。不同的虛擬機有不同的方式來實現GC,常見的有標記清除算法、標記整理算法、複製算法、分代收集算法,下面我們詳細介紹下各個算法的實現原理。

標記清除算法

上圖可以很明顯的看出來,標記清除算法分爲2步:標記出要回收的區域,然後清除這些空間內的垃圾。被清理出來的區域就變爲未使用的內存區域。

不足:標記清除會產生大量的內存碎片,可用的內存空間會被切割成多塊,而創建對象做內存分配時,需要的是連續的內存空間。如上圖所示,如果現在需要一塊4M的內存區域,其實是無法開闢出來的,但實際我們可用的內存空間總量是大於4M的。也就是我雖然有足夠的內存空間,實際卻不可用。

標記整理算法

如上圖,標記整理算法將所有存活的對象向一端移動,再清理掉邊界外的內存區域。這樣避免了內存碎片問題,未使用的內存空間都是持續的空間。

不足:標記整理算法對內存做了頻繁的變動,需要整理所有存活對象的引用地址並指向新的內存地址,性能和效率差很多。

複製算法

如上圖,複製算法是將內存區域分爲兩塊同等大小的區域,每次只使用其中一塊。當這一塊內存用完了,將存活着的對象遷移到另一塊未使用的區域,再將原有的區域的內存空間全部清理掉。複製算法解決了內存碎片問題,邏輯簡單,性能高效。

不足:總共14M的內存空間,實際可用的只有7M

分代收集算法

分代收集算法針對對象存活週期的不同,將堆內存分爲新生代和老年代,並針對這兩塊區域的特點採用最合適的收集算法。可以認爲分代收集算法是上述幾種算法的組合。

在新生代中,每次GC時都有絕大部分對象死亡,只有極少量存活,則可以使用複製算法,只需要小量的複製存活對象成本就可完成垃圾收集。老年代中,因爲存活的對象較多較大,沒有多餘的空間來做複製,一般使用標記整理算法來回收。

先看下分代收集的內存模型:

新生代

所有new出來的對象都會放在 Eden 區,當 Eden 區空間不足時,JVM發起 Minor GC,Eden 區中未被回收的存活對象移入 S0,如 S0 空間不足時,則再次 Minor GC,將存活的對象移入 S1,將S0的對象清除(複製算法),並記錄移入對象被GC的次數。下一次 Minor GC 時,S0 和 S1的職責互換,以此反覆。直到存活對象被GC的次數超過15時,纔會被移入老年代。

可以認爲,S0,S1就是老年代的一個緩存,且在緩存中的對象有極大可能在被GC15次之前被回收。而兩個 Survivor區(S0,S1),又可以保證總有一個區域是空閒的,而另一個是沒有碎片可使用的。據說不多分幾個 Survivor,是經過權衡後最佳的選擇。過多可能導致單個 Survivor 區域過小,容易被塞滿。

老年代

老年代佔據了堆內存中大部分的空間,只有在Major GC的時候纔會進行清理,但每次GC時會"stop-the-world",即其他線程都會被暫停。

需要注意,大對象需要連續的大量內存空間,因此被創建出來時就會直接進到老年代。避免在新生代中做大量的內存複製操作。

常用垃圾收集器

一張圖總結:Serial、Parallel Old、CMS、G1

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