GC回收機制及收集器

一、如何判斷是否可回收

1.引用計數法
給對象增加一個引用計數器,每當有一個地方引用它時,計數器就+1;當引用失效時,計數器就-1;任何時刻計數器爲0的對象就是不能再被使用的,即對象已"死"。
引用計數法實現簡單,判定效率也比較高,在大部分情況下都是一個不錯的算法。比如Python語言就採用引用計數法進行內存管理。
但是,在主流的JVM中沒有選用引用計數法來管理內存,最主要的原因就是引用計數法無法解決對象的循環引用問題

2.可達性分析
通過一系列稱爲"GC Roots"的對象作爲起始點,從這些節點開始向下搜索,搜索走過的路徑
稱之爲"引用鏈",當一個對象到GC Roots沒有任何的引用鏈相連時(從GC Roots到這個對象不可達)時,證明此對象是不可用的。以下圖爲例:
在這裏插入圖片描述對象Object5-Object7之間雖然彼此還有關聯,但是它們到GC Roots是不可達的,因此他們會被判定爲可回收對象。

擴展:四大引用

這四種引用的強度依次遞減:

  1. 強引用 : 強引用指的是在程序代碼之中普遍存在的,類似於"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的對象實例。
  2. 軟引用 : 軟引用是用來描述一些還有用但是不是必須的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出之前,會把這些對象列入回收範圍之中進行第二次回收。如果這次回收還是沒有足夠的內存,纔會拋出內存溢出異常。在JDK1.2之後,提供了SoftReference類來實現軟引用。
  3. 弱引用 : 弱引用也是用來描述非必需對象的。但是它的強度要弱於軟引用。被弱引用關聯的對象只能生存到下一次垃圾回收發生之前。當垃圾回收器開始進行工作時,無論當前內容是否夠用,都會回收掉只被弱引用關聯的對象。在JDK1.2之後提供了WeakReference類來實現弱引用。
  4. 虛引用 : 虛引用也被稱爲幽靈引用或者幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2之後,提供了PhantomReference類來實現虛引用。

二、回收方法區

方法區(永久代)的垃圾回收主要收集兩部分內容 : 廢棄常量和無用的類
無用常量舉例:
以常量池中字面量(直接量)的回收爲例,假如一個字符串"abc"已經進入了常量池中,但是當前系統沒有任何一個String對象引用常量池的"abc"常量,也沒有在其他地方引用這個字面量,如果此時發生GC並且有必要的話,這個"abc"常量會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

判定一個類是否是"無用類"則相對複雜很多。類需要同時滿足下面三個條件纔會被算是"無用的類" :

  1. 該類所有實例都已經被回收(即在Java堆中不存在任何該類的實例)
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的Class對象沒有在任何其他地方被引用,無法在任何地方通過反射訪問該類的方法

三、回收算法

1.標記清除算法

"標記-清除"算法是最基礎的收集算法。算法分爲"標記"和"清除"兩個階段 : 首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。後續的收集算法都是基於這種思路並對其不足加以改進而已。
在這裏插入圖片描述標記-清除"算法的不足主要有兩個 :

  1. 效率問題 : 標記和清除這兩個過程的效率都不高
  2. 空間問題 : 標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行中需要分配較大對象時,無法找到足夠連續內存而不得不提前觸發另一次垃圾收集

2.複製算法(新生代回收算法)

"複製"算法是爲了解決"標記-清理"的效率問題。它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這塊內存需要進行垃圾回收時,會將此區域還存活着的對象複製到另一塊上面,然後再把已經使用過的內存區域一次清理掉。這樣做的好處是每次都是對整個半區進行內存回收,內存分配時也就不需要考慮內存碎片等複雜情況,只需要移動堆頂指針,按順序分配即可。此算法實現簡單,運行高效。
在這裏插入圖片描述缺點:浪費內存,一半空間用不着

3.標記-整理算法(老年代回收算法)

複製收集算法在對象存活率較高時會進行比較多的複製操作,效率會變低。因此在老年代一般不能使用複製算法。
針對老年代的特點,提出了一種稱之爲"標記-整理算法"。標記過程仍與"標記-清除"過程一致,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然後直接清理掉端邊界以外的內存。
在這裏插入圖片描述

4.分代收集算法(重點)

當前JVM垃圾收集都採用的是"分代收集(Generational Collection)"算法,這個算法並沒有新思想,只是根據對象存活週期的不同將內存劃分爲幾塊。
一般是把Java堆分爲新生代和老年代。在新生代中,每次垃圾回收都有大批對象死去,只有少量存活,因此我們採用複製算法;而老年代中對象存活率高、沒有額外空間對它進行分配擔保,就必須採用"標記-清理"或者"標記-整理"算法。

  1. Minor GC又稱爲新生代GC : 指的是發生在新生代的垃圾收集。因爲Java對象大多都具備朝生夕滅的特性,因此Minor GC(採用複製算法)非常頻繁,一般回收速度也比較快。
  2. Full GC 又稱爲 老年代GC或者Major GC : 指發生在老年代的垃圾收集。出現了Major GC,經常會伴隨至少一次的Minor GC(並非絕對,在Parallel Scavenge收集器中就有直接進行Full
    GC的策略選過程)。Major GC的速度一般會比Minor GC慢10倍以上。

新生代:有三個區域:Eden(伊甸園)空間和兩塊較小的Survivor(倖存者)空間,每次使用Eden和其中一塊Survivor(兩個Survivor區域一個稱爲From區,另一個稱爲To區域)

新生代回收算法採用的是複製算法,但它的比例不是一半,而是8:1:1(Eden:From:To)
原因:新生代中98%的對象都是"朝生夕死"的,所以並不需要按照1 : 1的比例來劃分內存空間

  1. 當Eden區滿的時候,會觸發第一次Minor gc,把還活着的對象拷貝到Survivor From區;當Eden區再次觸發Minor gc的時候,會掃描Eden區和From區域,對兩個區域進行垃圾回收,經過這次回收後還存活的對象,則直接複製到To區域,並將Eden和From區域清空。
  2. 當後續Eden又發生Minor gc的時候,會對Eden和To區域進行垃圾回收,存活的對象複製到From區域,並將Eden和To區域清空。
  3. 部分對象會在From和To區域中複製來複制去,如此交換15次(由JVM參數MaxTenuringThreshold決定,這個參數默認是15),最終如果還是存活,就存入到老年代

當Survivor空間不夠用時,需要依賴其他內存(老年代)進行分配擔保。

四、回收器

在這裏插入圖片描述上圖展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明他們之間可以搭配使用。所處的區域,表示它是屬於新生代收集器還是老年代收集器。

並行(Parallel) : 指多條垃圾收集線程並行工作,用戶線程仍處於等待狀態
併發(Concurrent) :指用戶線程與垃圾收集線程同時執行(不一定並行,可能會交替執行),用戶程序繼續運行,而垃圾收集程序在另外一個CPU上。
吞吐量:就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值。

1.Serial收集器(新生代收集器,串行GC)

這個收集器是一個單線程的收集器,但它的“單線程”的意義並不僅僅說明它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束 (Stop The World)

在這裏插入圖片描述
Serial收集器是虛擬機運行在Client模式下的默認新生代收集器。

優勢:簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。 實際上到現在爲止 : 它依然是虛擬機運行在Client模式下的默認新生代收集器

2.ParNew收集器(新生代收集器,並行GC)

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集之外,其餘行爲包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的代碼。
在這裏插入圖片描述
實質:Serial收集器的多線程版本

場景:ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器

與Serial收集器對比:ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於存在線程交互的開銷,該收集器在通過超線程技術實現的兩個CPU的環境中都不能百分之百地保證可以超越Serial收集器。然而,隨着可以使用的CPU的數量的增加,它對於GC時系統資源的有效利用還是很有好處的。

3.Parallel Scavenge收集器(新生代收集器,並行GC)

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器。

它可以控制兩個參數:XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間 XX:GCRatio 直接設置吞吐量的大小

直觀上,只要最大的垃圾收集停頓時間越小,吞吐量是越高的,但是GC停頓時間的縮短是以犧牲吞吐量和新生代空間作爲代價的。比如原來10秒收集一次,每次停頓100毫秒,現在變成5秒收集一次,每次停頓70毫秒。停頓時間下降的同時,吞吐量也下降了。

場景:停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效率地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務

Parallel Scavenge收集器 VS ParNew收集器: Parallel Scavenge收集器與ParNew收集器的一個重要區別是它具有自適應調節策略。

GC自適應的調節策略: Parallel Scavenge收集器有一個參數- XX:+UseAdaptiveSizePolicy 。
當這個參數打開之後,就不需要手工指定新生代的大小、Eden與Survivor區的比例、晉升老年代對象年齡等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)。

4.Serial Old收集器(老年代收集器,串行GC)

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

應用場景:
Client模式:Serial Old收集器的主要意義也是在於給Client模式下的虛擬機使用。
Server模式:如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。
在這裏插入圖片描述

5.Parallel Old收集器(老年代收集器,並行GC)

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。

在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

這個收集器是在JDK 1.6中才開始提供的,在此之前,新生代的ParallelScavenge收集器一直處於比較尷尬的狀態。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了SerialOld收集器外別無選擇(ParallelScavenge收集器無法與CMS收集器配合工作)。由於老年代SerialOld收集器在服務端應用性能上的“拖累”,使用了ParallelScavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果,由於單線程的老年代收集中無法充分利用服務器多CPU的處理能力,在老年代很大而且硬件比較高級的環境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”。直到Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的應用組合

在這裏插入圖片描述

6. CMS收集器(老年代收集器,併發GC)

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大
一部分的Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMS收集器就非常符合這類應用的需求。

CMS收集器是基於“標記—清除”算法實現的,它的運作過程相對於前面幾種收集器來說更復雜一些,整個過程分爲

1.初始標記(CMS initial mark) 初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,需要“Stop The World”。
2.併發標記(CMS concurrent mark) 併發標記階段就是進行GC Roots Tracing的過程。
3.重新標記(CMS remark) 重新標記階段是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,仍然需要“Stop The World”。
4.併發清除(CMS concurrent sweep) 併發清除階段會清除對象。

由於整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。

Parallel Scavenge收集器 VS CMS等收集器: Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel
Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。 由於與吞吐量關係密切,
Parallel Scavenge收集器也經常稱爲“吞吐量優先”收集器。

優點: CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:併發收集、低停頓。

缺點:

  1. CMS收集器對CPU資源非常敏感其實,面向併發設計的程序都對CPU資源比較敏感。在併發階段,它雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程(或者說CPU資源)而導致應用程序變慢,總吞吐量會降低。 CMS默認啓動的回收線程數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大。
  2. CMS收集器無法處理浮動垃圾 CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。也是由於在垃圾收集階段用戶線程還需要運行,那也就還需要預留有足夠的內存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“ConcurrentMode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用SerialOld收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。 CMS收集器會產生大量空間碎片
  3. CMS是一款基於“標記—清除”算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。

在這裏插入圖片描述

7. G1收集器(唯一一款全區域的垃圾回收器)

G1(Garbage First)垃圾回收器是用在heap memory很大的情況下,把heap劃分爲很多很多的region塊,然後並行的對其進行垃圾回收。

G1垃圾回收器在清除實例所佔用的內存空間後,還會做內存壓縮。

G1垃圾回收器回收region的時候基本不會STW,而是基於 most garbage優先回收(整體來看是基於"標記-整理"算法,從局部(兩個region之間)基於"複製"算法) 的策略來對region進行垃圾回收的。

無論如何,G1收集器採用的算法都意味着一個region有可能屬於Eden,Survivor或者Tenured內存區域。圖中的E表示該region屬於Eden內存區域,S表示屬於Survivor內存區域,T表示屬於Tenured內存區域。圖中空白的表示未使用的內存空間。G1垃圾收集器還增加了一種新的內存區域,叫Humongous內存區域,如圖中的H塊。這種內存區域主要用於存儲大對象-即大小超過一個region大小的50%的對象。
在這裏插入圖片描述在G1垃圾收集器中,年輕代的垃圾回收過程使用複製算法。把Eden區和Survivor區的對象複製到新的Survivor區域。
在這裏插入圖片描述對於老年代上的垃圾收集,G1垃圾收集器也分爲4個階段,基本跟CMS垃圾收集器一樣,但略有不同:

1.初始標記(Initial Mark)階段 - 同CMS垃圾收集器的Initial Mark階段一樣,G1也需要暫停應用程序的執行,它會標記從根對象出發,在根對象的第一層孩子節點中標記所有可達的對象。但是G1的垃圾收集器 的Initial Mark階段是跟minorgc一同發生的。也就是說,在G1中,你不用像在CMS那樣,單獨暫停應 用程序的執行來運行Initial Mark階段,而是在G1觸發minor gc的時候一併將年老代上的Initial Mark給 做了。

2.併發標記(Concurrent Mark)階段 - 在這個階段G1做的事情跟CMS一樣。但G1同時還多做了一件事情, 就是如果在Concurrent Mark階段中,發現哪些Tenured region中對象的存活率很小或者基本沒有對象存活,那麼G1就會在這個階段將其回收掉,而不用等到後面的clean up階段。這也是Garbage First名字的由來。同時,在該階段,G1會計算每個 region的對象存活率,方便後面的clean up階段使用 。

3.最終標記(CMS中的Remark階段) - 在這個階段G1做的事情跟CMS一樣, 但是採用的算法不同,G1採用一 種叫做SATB(snapshot-at-the-begining)的算法能夠在Remark階段更快的標記可達對象。

4.篩選回收(Cleanup/Copy)階段 - 在G1中,沒有CMS中對應的Sweep階段。相反 它有一個Clean up/Copy階段,在這個階段中,G1會挑選出那些對象存活率低的region進行回收,這個階段也是和minor gc一同發生的,如下圖所示:

在這裏插入圖片描述

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