Java虛擬機垃圾回收過程及垃圾收集器

一、GC需要完成的事情

  1. 哪些內存需要回收?
  2. 什麼時候回收?
  3. 如何回收?

垃圾回收器所關注的是Java堆和方法區。

二、哪些內存需要回收?

Java堆中存放着Java世界中幾乎所有的對象實例,垃圾回收器在進行回收前要做的事情就是確定這些對象哪些還“存活”着,哪些已經“死去”(不可能再被任何途徑使用的對象),垃圾收集器將保留“存活”的對象,回收“死去”的對象的內存。

判斷對象是否存活的算法:

1、引用計數法

在創建對象時,爲對象創建一個伴生的引用計數器,當有其他對象引用該對象時,將引用計數器的值加1,如果其他對象不再引用該對象就將引用計數器的值減1,所以當引用計數器的值爲0時,就代表不再有任何對象引用該對象,就說明該對象已經”死亡“,就可以被判定爲待回收。

引用計數這種判定方法,實現比較簡單,判定的效率也比較高,但是主流的虛擬機裏面都沒有選擇這種方式。假如對象a和對象b相互進行了引用,其餘地方並沒有對兩個對象的引用了,但是兩塊內存的引用計數並不會爲0,所以不會將兩塊內存劃分爲待回收的內存。由於以上原因,大部分的虛擬機並不會採用這種方式進行對象”死亡“狀態判斷的方法。

2、可達性分析法

基本思想是定義一個GC Root對象,以GC Root爲起點,向下搜索,走過的路徑稱作引用鏈,當一個對象到GC Root之間沒有任何的引用鏈可以連接時,表示這個對象可以被回收了。
在這裏插入圖片描述

三、垃圾收集算法

在JVM中,進行垃圾回收主要有以下幾種方式:

  1. 標記-清除算法
  2. 複製算法
  3. 標記-整理算法
  4. 分代收集算法

1、標記-清除算法

標記-清除算法是最基礎的收集算法,該算法分成兩個過程:標記過程和清除過程。

首先標記出所有需要收集的對象,在標記完成後觸發垃圾回收,標記過程就是上面的兩種方法。

這種算法存在兩個明顯的問題:
第一個問題是效率問題。在這種算法中,標記和清除階段的效率都不是很高。
第二個問題就是空間問題,這種算法在標記清除之後會產生大量的內存碎片,也就是大量的可使用的小空間,但是這種小空間數量如果太多會導致程序在爲大對象分配內存空間時無法找到合適的內存,從而會觸發另外一次垃圾回收過程。

2、複製算法

由於標記-整理算法所存在的效率和空間問題,產生了第二種算法:複製算法。
原理是將內存劃分爲相等的兩塊,每次只使用其中的一塊,在第一塊內存使用完之後,觸發一次垃圾回收,將還存活的對象複製到另外一塊內存中去,然後將第一塊已使用的內存全部清除掉,之後的內存分配發生在第二塊中,當第二塊使用完成後,繼續重複上述步驟。

這樣就不需要考慮內存碎片問題,而且每次只回收其中的一塊內存,也讓效率得到了提高。但是這種算法的代價就是將內存分成了兩塊,每次只能使用其中一塊,所以在後來的分代算法中,只使用這種算法來回收新生代,而且兩塊內存的分配也不是各佔一半。

3、標記-整理算法

複製算法雖然解決了一些問題,但是在對象存活率較高的情況下就需要進行較多的複製操作,效率會變低,更重要的是如果我們不願意浪費一半的空間,就需要分配額外的空間來做內存擔保,以應對使用的內存中對象100%存活的極端情況。

在這種情況下,有人提出標記-整理算法,這種算法也是分爲兩個階段:標記階段和整理階段。標記階段我們不再詳細解釋,所謂的整理階段,就是在標記完成之後,讓所有存活的對象在內存中向一邊移動,將死亡對象擠出內存。

4、分代收集算法

分代收集算法就是標記-清除算法、標記-整理算法和複製算法的結合,基本思想是:
根據對象存活週期將Java堆內存分爲年輕代和老年代,年輕代使用複製算法,將年輕代分爲一個Eden區和兩個Survivor區(S0和S1區),內存佔比大體爲8:1:1。

每次只使用Eden區和一個Survivor區(如S0區),在需要回收時,就將存活的對象全部移動到另外一個Survivor區(S1),然後清除掉Eden區和第一個Survivor區(S0),下一次就使用Eden區和S1,將第二次GC的倖存者移到S0,清空Eden區和S1,如此反覆。

每次GC倖存下來的對象年齡+1,當年齡達到默認閾值15時,移動到老年代。

之所以在年輕代中選擇使用複製算法,是因爲在年輕代中的對象大多是朝生夕死的對象,存活率比較低,所以可以付出少量的內存作爲代價進行回收。但是老年代中對象的存活率相對於年輕代較高,也不存在額外的空間進行內存擔保,所以就使用標記-清除算法或者標記-整理算法進行收集。

四、對象內存分配

在這裏插入圖片描述

JAVA中的垃圾回收分爲Minor GC以及Full GC
Minor GC 意爲年輕代GC,由於年輕代對象大都爲朝生夕死的對象,所以Minor GC發生頻率比較頻繁,速度也比較快。
Full GC 爲老年代GC,但是當出現Full GC時,一般都會伴隨着至少一次的Minor GC,不過這種情況並非絕對,主要還是和垃圾收集器的收集策略有關。

1、對象優先分配在Eden

大多數情況下,對象的內存分配會發生在Eden區中,當Eden區沒有足夠的內存時,將會觸發一次Minor GC。

2、大對象直接進入老年代

所謂的大對象是指,需要大量連續內存空間的Java對象,最典型的大對象就是那種很長的字符串及數組。大對象對虛擬機的內存分配來說就是一個壞消息(比遇到一個大對象更加壞的消息就是遇到一羣“朝生夕死”的“短命大對象”,寫程序的時候應當避免),經常出現大對象容易導致內存還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。

3、長期存活的對象進入老年代

由於虛擬機會採用分代回收機制,所以就有必要識別哪些對象需要放在年輕代,哪些對象需要放在老年代中,在虛擬機中會爲對象定義一個年齡計數器,如果對象在Eden經歷過一次垃圾回收後還存活,移動至Survivor中,則對象年年齡加1,之後每經歷一次垃圾回收,如果對象還存活,則將對象年齡加1,當對象年齡達到設置閾值時,將被移動到老年代,默認閾值爲15,可以通過-XX:MaxTenuringThreshold來進行設置。

4、空間分配擔保

在發生Minor GC之前,虛擬機會檢查老年代中的最大的可用連續內存空間是否大於年輕代中所有存活對象的總空間,如果這個條件成立,則Minor GC是安全的,因爲老年代可以滿足所有年輕代存活對象進入老年代的極端情況。

如果不滿足,虛擬機則會查看HandlePromotionFailure設置是否允許擔保,如果允許,則檢查老年代中的最大的可用連續內存空間是否大於之前每次晉升老年代對象的平均大小,如果滿足就會觸發一次Minor GC,如果不滿足則會觸發一次Full GC。

但是這種策略在JDK6之後有了改變:老年代中的最大的可用連續內存空間如果大於之前每次晉升老年代對象的平均大小或者新生代中存活對象的總大小,則觸發一次Minor GC,否則進行Full GC。

其實歸根結底,JAVA中的內存分配和垃圾回收策略都是爲了使內存得到更高效的利用,並且希望將更多的內存管理交由機器來處理從而解放程序員勞動力,使程序員可以從複雜的內存管理中抽身並減少人爲管理內存中出現的錯誤。

五、垃圾收集器

如果說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。

1、年輕代垃圾收集器

1)Serial 收集器

Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器。

特點:

  1. 年輕代收集器
  2. 單線程收集器
  3. 複製算法
  4. “Stop The Word”
  5. 一般Serial 與老年代 Serial Old 組合使用

2)ParNew

ParNew垃圾收集器是Serial收集器的多線程版本, 除了多線程外,其餘的行爲、特點和Serial收集器一樣。

特點:

  1. 年輕代收集器
  2. 多線程收集器
  3. 複製算法
  4. 一般 ParNew 與老年代 CMS 組合使用

3)Parallel Scavenge收集器

Parallel Scavenge收集器又叫PS收集器,關注於吞吐量的收集器。
特點:

  1. 年輕代收集器
  2. 多線程收集器
  3. 複製算法
  4. 目標則是達一個可控制的吞吐量
  5. 一般 Parallel Scavenge 與老年代 Parallel Old 組合使用

2、老年代收集器

1)Serial Old 收集器

Serial Old是Serial 的老年代版本,同樣是一個單線程收集器,使用“標記-整理算法”。
特點:

  1. 老年代收集器
  2. 單線程收集器
  3. 標記整理算法
  4. “Stop The Word”

2)CMS收集器

CMS收集器是一種以獲取最短回收停頓時間爲目標的收集器。使用“標記-清除算法”。

運作過程:
1、初始標記:標記GCRoot能直接關聯的對象,“Stop The Word”。
2、併發標記:標記所有的Old對象
3、重新標記:修正 併發標記期間因用戶程序繼續運作而沒有標記到的那部分對象,“Stop The Word”。
4、併發清除:使用標記-清除算法。

3)Parallel Old 收集器

Parallel Old 是 Parallel Scavenge的老年代版本,使用多線程和“標記-整理算法”。特點跟年輕代的差不多。

3、整堆收集器

G1 收集器

//TODO

發佈了69 篇原創文章 · 獲贊 12 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章