java垃圾回收機制詳解

一、按代實現垃圾回收

先看一下整體結構:


新生代(Young generation): 絕大多數最新被創建的對象會被分配到這裏,由於大部分對象在創建後會很快變得不可到達,所以很多對象被創建在新生代,然後消失。對象從這個區域消失的過程我們稱之爲”minor GC“。

新生代中存在一個Eden區和兩個Survivor區。新對象會首先分配在 Eden 中(如果新對象過大,會直接分配在老年代中)。在GC中,Eden 中的對象會被移動到survivor中,直至對象滿足一定的年紀(定義爲熬過GC的次數),會被移動到老年代(具體細節將在下邊垃圾收集算法中討論)。

可以設置新生代和老年代的相對大小。這種方式的優點是新生代大小會隨着整個堆大小動態擴展。參數 -XX:NewRatio 設置老年代與新生代的比例。例如 -XX:NewRatio=8 指定老年代/新生代爲8/1. 老年代佔堆大小的 7/8 ,新生代佔 1/8 .(默認即使1/8)

例如:-XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8

老年代(Old generation): 對象沒有變得不可達,並且從新生代中存活下來,會被拷貝到這裏。其所佔用的空間要比新生代多。也正由於其相對較大的空間,發生在老年代上的GC要比新生代少得多。對象從老年代中消失的過程,可以稱之爲”major GC“(或者”full GC“)

永久代(permanent generation):像一些類的層級信息,方法數據和方法信息(如字節碼,棧和變量大小),運行時常量池(jdk7之後移出永久代),已確定的符號引用和虛方法表等等,它們幾乎都是靜態的並且很少被卸載和回收,在JDK8之前的HotSpot虛擬機中,類的這些“永久的”數據存放在一個叫做永久代的區域。永久代一段連續的內存空間,我們在JVM啓動之前可以通過設置-XX:MaxPermSize的值來控制永久代的大小。但是jdk8之後取消了永久代,這些元數據被移到了一個與堆不相連的本地內存區域(詳情參考:http://www.infoq.com/cn/articles/Java-PERMGEN-Removed)

二、怎樣判斷對象是否已經死亡

引用計數收集算法

用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象(不是引用)都有一個引用計數。當一個對象被創建時,且將該對象分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則b引用的對象+1),但當一個對象的某個引用超過了生命週期或者被設置爲一個新值時,對象的引用計數減1。任何引用計數爲0的對象可以被當作垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。

優點:引用計數收集器可以很快的執行,交織在程序運行中。對程序不被長時間打斷的實時環境比較有利。

缺點: 無法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0.

可達性分析算法

通過一系列稱爲”GC Roots”的對象作爲起點,從這些節點開始向下搜索,搜索所有走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到此對象不可達),則證明此對象是不可用的。

可作爲GC Roots的對象包括:

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

三、java中的引用

  • 強引用(Strong Reference): 在代碼中普遍存在的,類似”Object obj = new Object”這類引用,只要強引用還在,垃圾收集器永遠不會回收掉被引用的對象
  • 軟引用(Sofe Reference): 有用但並非必須的對象,可用SoftReference類來實現軟引用,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存異常異常。
  • 弱引用(Weak Reference): 被弱引用關聯的對象只能生存到下一次垃圾收集發生之前,JDK提供了WeakReference類來實現弱引用。
  • 虛引用(Phantom Reference):也稱爲幽靈引用或幻影引用,是最弱的一種引用關係,JDK提供了PhantomReference類來實現虛引用。

四、finalize方法什麼作用

對於一個對象來說,在被判斷沒有 GCroots 與其相關聯時,被第一次標記,然後判斷該對象是否應該執行finalize方法(判斷依據:如果對象的finalize方法被複寫,並且沒有執行過,則可以被執行)。如果允許執行那麼這個對象將會被放到一個叫F-Query的隊列中,等待被執行。(注意:由於finalize的優先級比較低,所以該對象的的finalize方法不一定被執行,即使被執行了,也不保證finalize方法一定會執行完)

五、垃圾收集算法

標記-清除算法:標記-清除算法採用從根集合進行掃描,對存活的對象進行標記,標記完畢後,再掃描整個空間中未被標記的對象,進行回收。標記-清除算法不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象比較多的情況下極爲高效,但由於標記-清除算法直接回收不存活的對象,因此會造成內存碎片。

複製算法:這種收集算法將堆棧分爲兩個域,常稱爲半空間。每次僅使用一半的空間,JVM生成的新對象則放在另一半空間中。GC運行時,它把可到達對象複製到另一半空間,從而壓縮了堆棧。這種方法適用於短生存期的對象,持續複製長生存期的對象則導致效率降低。並且對於指定大小堆來說,需要兩倍大小的內存,因爲任何時候都只使用其中的一半。

標記整理算法:標記-整理算法採用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象佔用的空間後,會將所有的存活對象往一端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。

分代收集算法:在上邊三種收集思想中加入了分代的思想。

六、Hotspot實現垃圾回收細節

一致性:在可達性分析期間整個系統看起來就像被凍結在某個時間點上,不可以出現分析過程中對象引用關係還在不斷變化的情況。

一致性要求導致GC進行時必須停頓所有Java執行線程。(Stop The World)即使在號稱不會發生停頓的CMS收集器中,枚舉根節點時也是必須停頓的。

HotSpot使用的是準確式GC,當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全局的引用位置,這是通過一組稱爲OopMap的數據結構來達到的。

安全點(Safe Point):程序只有在到達安全點時才能暫停。安全點的選定標準是“是否具有讓程序長時間執行的特徵”。“長時間執行”的最明顯特徵就是指令序列的複用,如方法調用、循環跳轉等,具有這些功能的指令纔會產生安全點。

讓程序暫停的兩種方式

  • 搶先式中斷(Preemptive Suspension):在GC發生時,主動中斷所有線程,不需要線程執行的代碼主動配合。如果發現有線程中斷的地方不在安全點上,就恢復線程讓它跑到安全點上。(不推薦)
  • 主動式中斷(Voluntary Suspension):設一個標誌,各個線程主動去輪詢這個標誌,遇到中斷則暫停。輪詢地方與安全點重合。

七、垃圾收集器

HotSpot中幾種常見的垃圾收集器:


1、Serial收集器:Serial收集器是最基本、發展歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是虛擬機新生代收集的唯一選擇。


特性:

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

應用場景:

Serial收集器是虛擬機運行在Client模式下的默認新生代收集器。

優勢:

簡單而高效(與其他收集器的單線程比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

2、ParNew收集器


特性:

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

應用場景:

ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器。有一個很重要的原因是除了Serial收集器外,目前只有它能與CMS收集器配合工作。

Serial收集器 VS ParNew收集器:

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

3、Parallel Scavenge收集器

特性:

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

應用場景:

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

對比分析:

Parallel Scavenge收集器 VS CMS等收集器:

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關 注點是儘可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。

由於與吞吐量關係密切,Parallel Scavenge收集器也經常稱爲“吞吐量優先”收集器。

Parallel Scavenge收集器 VS ParNew收集器:

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

GC自適應的調節策略:

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

4、Serial Old收集器


特性:

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

應用場景:

Client模式:Serial Old收集器的主要意義也是在於給Client模式下的虛擬機使用。

Server模式:如果在Server模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

5、Parallel Old收集器


特性:

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

應用場景:

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

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

6、CMS收集器


特性:

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

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

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

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

優點:

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運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。

3)CMS收集器會產生大量空間碎片

CMS是一款基於“標記—清除”算法實現的收集器,這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前對象,不得不提前觸發一次Full GC。

7、G1收集器


特性:

G1(Garbage-First)是一款面向服務端應用的垃圾收集器。HotSpot開發團隊賦予它的使命是未來可以替換掉JDK 1.5中發佈的CMS收集器。與其他GC收集器相比,G1具備如下特點。

並行與併發

G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓Java程序繼續執行。

分代收集

與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。

空間整合

與CMS的“標記—清理”算法不同,G1從整體來看是基於“標記—整理”算法實現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的,但無論如何,這兩種算法都意味着G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次GC。

可預測的停頓

這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存佈局就與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

G1收集器之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

執行過程:

G1收集器的運作大致可劃分爲以下幾個步驟:

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

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

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

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


博文轉自:http://toutiao.com/a6245610074174456066/

最近也在看《深入理解Java虛擬機》一書,覺得此書寫的很詳細,可能此篇博文就是按照那本書來寫的吧,博文總結的挺好的。值得收藏。




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