JavaGC垃圾回收機制和常見垃圾回收算法


 

Java GC是在什么時候,對什么東西,做了什么事情?”

 

什么位置:大部分在還有方法區!!方法區的垃圾收集主要回收兩部分內容:廢棄常量無用的類,當滿了之后同樣觸發FullGC, HotSpot1.8之前由永久代實現,1.8已經移到元空間元空間並不在虛擬機中,而是使用本地內存

 

什么時候:程序員不能控制具體時間,系統在不可預測的時間調用System.gc()函數的時候;當然可以通過調優,用NewRatio控制newObject和oldObject的比例,用MaxTenuringThreshold 控制進入oldObject的次數,使得oldObject 存儲空間延遲達到full gc,從而使得計時器引發gc時間延遲OOM的時間延遲,以延長對象生存期。

 

什么東西:超出了作用域或引用計數為空的對象;從gc root開始搜索找不到的對象,而且經過一次標記、清理,仍然沒有復活的對象。

 

什么事情:刪除不使用的對象,回收內存空間;運行默認的finalize,當然程序員想立刻調用就用dipose調用以釋放資源如文件句柄,JVM用from survivor、to survivor對它進行標記清理,對象序列化后也可以使它復活。

 

引用計數法和可達性分析算法

引用計數法(ReferenceCounting):給對象中添加一個引用計數器,每當它被引用到一個地方時,計數器值就+1,;當引用失效時,計數器值就-1;任何時刻計數器為0的對象就是不可能在被使用

1)、優點

判定效率很高

(2)、缺點

不會完全准確,因為如果出現兩個對象相互引用的問題就不行了。

就像JVM問A可以被回收不,A說我被B引用去問B;

JVM問B可以被回收不,B說我被A引用去問A。

現在虛擬機都不采用引用計數法

 

 

可達性分析法:該方法的基本思想是通過一系列的GC Roots對象(局部變量,棧等)作為起點進行搜索,如果在“GC Roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的,不過要注意的是被判定為不可達的對象不一定就會成為可回收對象;被判定為不可達的對象要成為可回收對象必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收對象的可能性,則基本上就真的成為可回收對象了。

 

GC Roots的對象

  • 虛擬機棧(幀棧中的本地變量表)中引用的對象
  • 方法區靜態屬性引用的對象。
  • 方法區常量引用的對象。
  • 本地方法棧 JNI(Java Native Interface) 引用的對象。

 

Stop The World

可達性分析期間需要保證整個執行系統的一致性,對象的引用關系不能發生變化,所以需要將用戶的正常的工作線程全部停掉,避免對象的引用關系變化,與可達性分析不一致

 

導致GC進行時必須停頓所有Java執行線程(稱為"Stop The World");(幾乎不會發生停頓的CMS收集器中,枚舉GC ROOTS時也是必須要停頓的)

      是JVM在后台自動發起自動完成的;

      在用戶不可見的情況下,把用戶正常的工作線程全部停掉;

 

 

垃圾回收算法

標記-清除(Mark-Sweep)算法兩個階段標記階段和清除階段。標記階段的任務是根據GC ROOTS標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間;標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致后續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

 

 

復制算法它將可用內存按容量划分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象復制到另外一塊上面,然后再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。

 

 

標記-整理算法:應用在老年代該算法標記階段和Mark-Sweep一樣,但是在完成標記之后,它不是直接清理可回收對象,而是將存活對象一端移動,然后清理掉端邊界以外的內存。在清理的時候,把所有 存活 對象扎堆到同一個地方,讓它們待在一起,這樣就沒有內存碎片了。

 

 

分代收集算法(目前常用):應用在年輕代根據對象存活的生命周期將內存划分為若干個不同的區域一般來說是將新生代划分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將EdenSurvivor中還存活的對象復制到另一塊Survivor空間中,然后清理掉Eden和剛才使用過的Survivor空間。

 

 

新生代一個Eden、兩個survivor區,對Eden和其中一個survivor Minor GC,再復制另一個Survivor區,目的是減少內存碎片

老年代只有在 Major GC 的時候才會進行清理,每次 GC 都會觸發STW(Stop-The-World),使用標記-整理算法。

 

分代收集算法總結:

G1垃圾回收器應用了該分代收集算法

(1)在年輕代中,Eden區提供堆內存如果滿了,Eden進行MinorGC將存活的對象Survivor A中,Eden區清空

2)Eden再次滿 Eden 區和 Survivor A 區同時進行 Minor GC,把存活對象放入 Survivor B 區,Eden和Survivor A同時清空;

(3)重復2)的操作,如果當某個 Survivor 區被填滿,且仍有對象未被復制完畢時,或者某些對象在反復 Survive 15 次左右時,或者大對象,則把這部分剩余對象放到Old 區(老年代)

(4)當 Old 區也被填滿時,進行 Full GC,對 Old 區進行垃圾回收。

 

[注意,在真實的 JVM 環境里,可以通過參數 SurvivorRatio 手動配置 Eden 區和單個 Survivor 區的比例,默認為 8:1:1。可以通過參數–XX:SurvivorRatio 來設定,即將堆內存中年輕代划分為8:1:1]

 

收集器

接下里會介紹在HotSpot虛擬機中常用的幾種垃圾收集器,垃圾收集器垃圾回收算法的具體實現,不同的商家、不同版本的JVM所提供的垃圾收集器可能會存在差異。

這幾種收集器分別是Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。在了解垃圾收集器之前,我們先來區分幾個概念:

(重點)並發收集器VS並行收集器
  並行:指多條收集線程同時進行收集工作,但此時用戶線程處於等待狀態。如ParNewParallel ScavengeParallel Old
  並發:指用戶線程與垃圾收集線程同時執行(並不一定是並行,可能會交替執行)。如CMS、G1

YoungGC VS OldGC VS MinorGC VS MajorGC VS FullGC
  Minor GC、YoungGC:Minor GC又稱為新生代GC,所以等價於Young GC,在新生代的Eden區分配滿的時候觸發。在Young GC后新生代中有部分存活對象會晉升到老年代,有可能是年齡達到閾值(默認為15歲,在JVM里面15歲就步入老年生活了,O(∩_∩)O哈哈~)了,也可能是Survivor區域滿了,如果是Survivor區域被填滿,會將所有新生代中存活的對象移動到老年代中!

  Major GC、Old GC、Full GC:Old GC從字面能理解是老年代的GC,但是對Major GC和Full GC存在多種說法,有的認為Major GC等價於Old GC只是針對老年代的GC,有的認為Major GC和Full GC是等價的。但是我個人認為Major是指老年代GC,而Full GC針對新生代老年代方法區整個的回收。

  由於老年代的GC都會伴隨一次新生代的GC,所以習慣性的把Major GC和Full GC划上了等號。前面Young GC時候說到“在Young GC后新生代中有部分存活對象會晉升到老年代”,萬一老年代的空間不夠存放新生代晉升的對象怎么辦呢?所以當准備要觸發一次Young GC時,如果發現統計數據之前Young GC的平均晉升大小比目前老年代剩余的空間大,則不會單獨觸發Young GC,而是轉為觸發Full GC,也就是堆和方法區的收集!

常見垃圾回收器:

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

主要包含SerialSerrial Old兩種收集器,分別用來收集新生代和老年代。

串行收集器由於是單線程收集,在進行垃圾收集時,必須暫停(Stop The World)所有的工作線程,直到GC線程工作完成。運行示意圖如下:

Serial 收集器:主要針對新生代回收,采用復制算法,單線程收集。
Serial Old收集器:主要針對老年代回收,采用“標記-整理”算法,單線程收集。

串行收集器在單CPU的環境下,沒有線程切換的開銷,可以獲得最高的單線程收集效率,但是由於現在普遍都是多CPU(或者多核)環境,所以除了在桌面應用中仍然將串行收集器作為默認的收集器,其他場景已經很少(很少不代表沒有,后面CMS會講到)使用。

在上面我們談到一個詞,需要暫停(Stop The World)所有的工作線程,這個概念在后面也會多次提到,為什么需要暫停呢?一是為了方便GC動作,不然在GC過程中又會額外產生新的垃圾,或者分配新的對象。二是因為GC過程中對象的地址會發生變化,如果不暫停線程,可能會導致引用出現問題。

 

並行收集器是串行收集器的多線程版本,除了多線程外,其余的行為、特點和串行收集器一樣。主要包含ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器。

JDK1.6~1.8默認使用了Parallel Scavenge(年輕代收集器)Parallel Old(老年代回收器)

運行示意圖如下:

ParNew收集器:主要針對新生代回收,采用復制算法,多線程收集。一般老年代如果使用CMS收集器,則默認會使用ParNew作為新生代收集器。


Parallel Scavenge收集器:該收集器與ParNew收集器類似,也是新生代收集器,采用復制算法,多線程收集。其他收集器關注點是盡可能地縮短垃圾收集時用戶線程停頓的時間,但是Parallel Scavenge收集器的目標則是達到一個可控的吞吐量(吞吐量=CPU運行用戶代碼時間/(CPU運行用戶代碼時間+CPU垃圾收集時間)),所以該收集器也成為吞吐量收集器。由於該收集器沒有使用傳統的GC收集器代碼框架,是另外獨立實現的,所以無法和CMS收集器配合工作。


Parallel Old收集器:主要針對老年代回收,采用“標記-整理”算法,多線程收集。該收集器是Parallel Scavenge收集器的老年代版本。在JDK1.6之后用來替代老年的Serial Old收集器。在注重吞吐量以及CPU資源敏感的場景,一般會選擇Parallel Scavenge+Parallel Old的組合進行垃圾收集。

 

CMS

 

CMSConcurrent Mark-Sweep Collector)收集器是一種以獲取最短回收停頓時間為目標的收集器,它是一種並發收集器,CPUCMS並發標記並發清除階段會與用戶線程切換執行,采用的是Mark-Sweep算法。

一種以獲取最短回收停頓時間為目標的收集器標記-清除,有 4 個過程:

初始標記(查找直接 gc roots 鏈接的對象),需要“Stop The World”;

並發標記GC Roots Tracing 過程:查找與gc roots非直接相連的對象,以GCRoots的對象作為起始點,從這個節點向下搜索,搜索走過的路徑稱為ReferenceChain,當一個對象到GCRoots沒有任何ReferenceChain相連時,這個對象不可到達,則證明這個對象不可用

重新標記(因 並發標記時有用戶線程在執行,標記結果可能有變化),需要“Stop The World

並發清除(並發清除階段會清除對象

 

其中初始標記和重新標記階段,要stop the world(停止工作線程)

 (優缺點都很重要)

優點並發收集,低停頓,所以CMS收集器適合與用戶交互較多的場景,注重服務的響應速度,能給用戶帶來較好的體驗!所以我們在做WEB開發的時候,經常會使用CMS收集器作為老年代的收集器!

 

缺點

1能處理浮動垃圾 (在最后一步並發清理階段,用戶線程還在運行,這時候可能就會又有新的垃圾產生,而無法此次GC過程中被回收,這成為浮動垃圾)

2)對 cpu 資源敏感,占用CPU資源較大。CMS默認啟動的回收線程數是(CPU數量+3)/ 4,也就是當CPU在4個以上時,並發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,CMS對用戶程序的影響就可能變得很大

3)產生大量內存碎片(因為使用的是標記-清除算法)

CMS收集器采用“標記-清除”算法,在清除后不會進行壓縮操作,這樣會導致產生大量不連續的內存碎片,在分配大對象時,無法找到足夠的連續內存,從而需要提前觸發一次FullGC的動作。針對該問題,提供了兩個參數來設置是否開啟碎片整理。
  1)、“-XX:+UseCMSCompactAtFullCollection”參數
  從名字能看出來,在收集的時候是否開啟壓縮。這個參數默認是開啟的,但是是否開啟壓縮還需要結合下面的參數!
  2)、“-XX:+CMSFullGCsBeforeCompaction”參數
  該參數設置執行多少次不壓縮的Full GC后,來一次壓縮整理。這個參數默認為0,也就是說每次都執行Full GC,不會進行壓縮整理。

  要根據實際情況合理設計該參數,因為CMS要的就是低停頓,如果CMS多線程一起整理垃圾對象需要Stop the world所需時間過長,反而會導致高停頓,違背了設計初衷低停頓的特點
  如果開啟了壓縮(整理對象),則在清理階段需要“Stop the world”,不能進行並發

4)“Concurrent Mode Failure”失敗
不知道大家在開發過程中有沒有遇到過“Concurrent Mode Failure”失敗的信息,這個異常是什么原因導致的呢。

並發標記並發清除階段,用戶線程GC線程並發工作,這會導致在清理的時候又會有用戶的線程在拼命的創建對象,本身垃圾回收時候肯定是可用內存不夠了,可萬一這時候用戶線程創建了大量的對象怎么辦呢?所以一般CMS收集器的垃圾回收的動作不會在完全無法分配內存的時候進行,可以通過“-XX:CMSInitiatingOccupancyFraction”參數來設置CMS預留的內存空間

如果預留的空間無法滿足CMS的需要,就會出現 “Concurrent Mode Failure”失敗。

這時候JVM會啟用后備方案,也就是前面介紹過的Serial Old收集器,這樣會導致另一次的Full GC的產生,這樣的代價是很大的,所以CMSInitiatingOccupancyFraction這個參數設置需要根據程序合理設置!

 

 

G1

 

JDK1.9開始默認使用G1回收器,全量回收器,當今JAVA最好的回收器,充分利用CPU並且不存在內存碎片,原理是將內存切分成多個相同大小的區域,標記-整理時也是按區域進行,所以當回收時會采用分區回收最大程度的降低了STOP-THE-WORLD情況。

G1收集器是面向服務端應用的收集器,它能充分利用CPU多核環境。

因此它是一款並行並發收集器,並且它能建立可預測的停頓時間模型。

垃圾回收進行了划分優先級的操作,這種有優先級的區域回收方式保證了它的高效率;最大的優點是結合了空間整合不會產生大量的碎片,也降低了進行gc的頻率,讓使用者明確指定停頓時間。

根據用戶設定的GC停頓時間的多少,在最后"篩選回收"階段,按照區域優先級回收,對優先級高的階段優先進行回收,停頓時間不夠,則只回收一部分區域。

 

初始標記(Initial Marking) 
初始標記階段僅僅只是標記一下GC Roots直接關聯到的對象,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可用的Region中創建新對象,這階段需要停頓線程,但耗時很短。

並發標記(Concurrent Marking) 
並發標記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序並發執行。

最終標記(Final Marking) 
最終標記階段是為了修正在並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set Logs里面,最終標記階段需要把Remembered Set Logs的數據合並到Remembered Set中,這階段需要停頓線程,但是可並行執行。

篩選回收(Live Data Counting and Evacuation) 
篩選回收階段首先對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划,這個階段其實也可以做到與用戶程序一起並發執行,但是因為只回收一部分Region時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率(分代收集算法?)。

 

 來源:

https://baijiahao.baidu.com/s?id=1583441733083989684&wfr=spider&for=pc

http://blog.csdn.net/cy609329119/article/details/51771953

https://blog.csdn.net/xiaomingdetianxia/article/details/77446762

https://juejin.im/post/5ad5c0216fb9a028e014fb63

http://www.17coding.info/article/16

https://blog.csdn.net/wmq880204/article/details/85320873


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM