天天看點

深入了解JVM虛拟機2:JVM垃圾回收基本原理和算法

本文轉自網際網路,侵删

本系列文章将整理到我在GitHub上的《Java面試指南》倉庫,更多精彩内容請到我的倉庫裡檢視

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點下Star哈

文章将同步到我的個人部落格:

www.how2playlife.com

本文是微信公衆号【Java技術江湖】的《深入了解JVM虛拟機》其中一篇,本文部分内容來源于網絡,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格内容,引用其中了一些比較好的部落格文章,如有侵權,請聯系作者。

該系列博文會告訴你如何從入門到進階,一步步地學習JVM基礎知識,并上手進行JVM調優實戰,JVM是每一個Java工程師必須要學習和了解的知識點,你必須要掌握其實作原理,才能更完整地了解整個Java技術體系,形成自己的知識架構。

為了更好地總結和檢驗你的學習成果,本系列文章也會提供每個知識點對應的面試題以及參考答案。

如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公衆号【Java技術江湖】聯系作者,歡迎你參與本系列博文的創作和修訂。

<!-- more -->

Java的記憶體配置設定與回收全部由JVM垃圾回收程序自動完成。與C語言不同,Java開發者不需要自己編寫代碼實作垃圾回收。這是Java深受大家歡迎的衆多特性之一,能夠幫助程式員更好地編寫Java程式。

下面四篇教程是了解Java 垃圾回收(GC)的基礎:

垃圾回收簡介

圾回收是如何工作的?

垃圾回收的類别

這篇教程是系列第一部分。首先會解釋基本的術語,比如JDK、JVM、JRE和HotSpotVM。接着會介紹JVM結構和Java 堆記憶體結構。了解這些基礎對于了解後面的垃圾回收知識很重要。

JavaAPI:一系列幫助開發者建立Java應用程式的封裝好的庫。

Java 開發工具包 (JDK):一系列工具幫助開發者建立Java應用程式。JDK包含工具編譯、運作、打包、分發和監視Java應用程式。

Java 虛拟機(JVM):JVM是一個抽象的計算機結構。Java程式根據JVM的特性編寫。JVM針對特定于作業系統并且可以将Java指令翻譯成底層系統的指令并執行。JVM確定了Java的平台無關性。

Java 運作環境(JRE):JRE包含JVM實作和Java API。

每種JVM實作可能采用不同的方法實作垃圾回收機制。在收購SUN之前,Oracle使用的是JRockit JVM,收購之後使用HotSpot JVM。目前Oracle擁有兩種JVM實作并且一段時間後兩個JVM實作會合二為一。

HotSpot JVM是目前Oracle SE平台标準核心元件的一部分。在這篇垃圾回收教程中,我們将會了解基于HotSpot虛拟機的垃圾回收原則。

我們有必要了解堆記憶體在JVM記憶體模型的角色。在運作時,Java的執行個體被存放在堆記憶體區域。當一個對象不再被引用時,滿足條件就會從堆記憶體移除。在垃圾回收程序中,這些對象将會從堆記憶體移除并且記憶體空間被回收。堆記憶體以下三個主要區域:

新生代(Young Generation)

Eden空間(Eden space,任何執行個體都通過Eden空間進入運作時記憶體區域)

S0 Survivor空間(S0 Survivor space,存在時間長的執行個體将會從Eden空間移動到S0 Survivor空間)

S1 Survivor空間 (存在時間更長的執行個體将會從S0 Survivor空間移動到S1 Survivor空間)

老年代(Old Generation)執行個體将從S1提升到Tenured(終身代)

永久代(Permanent Generation)包含類、方法等細節的元資訊

永久代空間在Java SE8特性中已經被移除。

Java 垃圾回收是一項自動化的過程,用來管理程式所使用的運作時記憶體。通過這一自動化過程,JVM 解除了程式員在程式中配置設定和釋放記憶體資源的開銷。

作為一個自動的過程,程式員不需要在代碼中顯示地啟動垃圾回收過程。<code>System.gc()</code>和<code>Runtime.gc()</code>用來請求JVM啟動垃圾回收。

雖然這個請求機制提供給程式員一個啟動 GC 過程的機會,但是啟動由 JVM負責。JVM可以拒絕這個請求,是以并不保證這些調用都将執行垃圾回收。啟動時機的選擇由JVM決定,并且取決于堆記憶體中Eden區是否可用。JVM将這個選擇留給了Java規範的實作,不同實作具體使用的算法不盡相同。

毋庸置疑,我們知道垃圾回收過程是不能被強制執行的。我剛剛發現了一個調用<code>System.gc()</code>有意義的場景。通過這篇文章了解一下适合調用System.gc() 這種極端情況。

說到GC類型,就更有意思了,為什麼呢,因為業界沒有統一的嚴格意義上的界限,也沒有嚴格意義上的GC類型,都是左邊一個教授一套名字,右邊一個作者一套名字。為什麼會有這個情況呢,因為GC類型是和收集器有關的,不同的收集器會有自己獨特的一些收集類型。是以作者在這裡引用R大關于GC類型的介紹,作者覺得還是比較妥當準确的。如下:

Partial GC:并不收集整個GC堆的模式

Young GC(Minor GC):隻收集young gen的GC

Old GC:隻收集old gen的GC。隻有CMS的concurrent collection是這個模式

Mixed GC:收集整個young gen以及部分old gen的GC。隻有G1有這個模式

Full GC(Major GC):收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

上面大家也看到了,GC類型分分類是和收集器有關的,那麼當然了,對于不同的收集器,GC觸發時機也是不一樣的,作者就針對預設的serial GC來說:

young GC:當young gen中的eden區配置設定滿的時候觸發。注意young GC中有部分存活對象會晉升到old gen,是以young GC後old gen的占用量通常會有所升高。

full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,是以不需要事先觸發一次單獨的young GC);或者,如果有perm gen的話,要在perm gen配置設定空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。

除直接調用System.gc外,觸發Full GC執行的情況有如下四種。

1. 舊生代空間不足

舊生代空間隻有在新生代對象轉入及建立為大對象、大數組時才會出現不足的現象,當執行Full GC後空間仍然不足,則抛出如下錯誤:

java.lang.OutOfMemoryError: Java heap space 

為避免以上兩種狀況引起的Full GC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。

2. Permanet Generation空間滿

Permanet Generation中存放的為一些class的資訊等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被占滿,在未配置為采用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會抛出如下錯誤資訊:

java.lang.OutOfMemoryError: PermGen space 

為避免Perm Gen占滿造成Full GC現象,可采用的方法為增大Perm Gen空間或轉為使用CMS GC。

3. CMS GC時出現promotion failed和concurrent mode failure

對于采用CMS進行舊生代GC的程式而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能會觸發Full GC。

promotion failed是在進行Minor GC時,survivor space放不下、對象隻能放入舊生代,而此時舊生代也放不下造成的;concurrent mode failure是在執行CMS GC的過程中同時有對象要放入舊生代,而此時舊生代空間不足造成的。

應對措施為:增大survivor space、舊生代空間或調低觸發并發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由于JDK的bug29導緻CMS在remark完畢後很久才觸發sweeping動作。對于這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(機關為ms)來避免。

4. 統計得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩餘空間

這是一個較為複雜的觸發情況,Hotspot為了避免由于新生代對象晉升到舊生代導緻舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程式第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大于6MB,如果小于6MB,則執行Full GC。

當新生代采用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大于6MB,如小于,則觸發對舊生代的回收。

除了以上4種狀況外,對于使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java -Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

 Minor GC ,Full GC 觸發條件

Minor GC觸發條件:當Eden區滿時,觸發Minor GC。

Full GC觸發條件:

(1)調用System.gc時,系統建議執行Full GC,但是不必然執行

(2)老年代空間不足

(3)方法去空間不足

(4)通過Minor GC後進入老年代的平均大小大于老年代的可用記憶體

(5)由Eden區、From Space區向To Space區複制時,對象大小大于To Space可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小于該對象大小

Java中Stop-The-World機制簡稱STW,是在執行垃圾收集算法時,Java應用程式的其他所有線程都被挂起(除了垃圾收集幫助器之外)。Java中一種全局暫停現象,全局停頓,所有Java代碼停止,native代碼可以執行,但不能與JVM互動;這些現象多半是由于gc引起。

GC時的Stop the World(STW)是大家最大的敵人。但可能很多人還不清楚,除了GC,JVM下還會發生停頓現象。

JVM裡有一條特殊的線程--VM Threads,專門用來執行一些特殊的VM Operation,比如分派GC,thread dump等,這些任務,都需要整個Heap,以及所有線程的狀态是靜止的,一緻的才能進行。是以JVM引入了安全點(Safe Point)的概念,想辦法在需要進行VM Operation時,通知所有的線程進入一個靜止的安全點。

除了GC,其他觸發安全點的VM Operation包括:

1. JIT相關,比如Code deoptimization, Flushing code cache ;

2. Class redefinition (e.g. javaagent,AOP代碼植入的産生的instrumentation) ;

3. Biased lock revocation 取消偏向鎖 ;

4. Various debug operation (e.g. thread dump or deadlock check);

垃圾回收是一種回收無用記憶體空間并使其對未來執行個體可用的過程。

Eden 區:當一個執行個體被建立了,首先會被存儲在堆記憶體年輕代的 Eden 區中。

注意:如果你不能了解這些詞彙,我建議你閱讀這篇 垃圾回收介紹 ,這篇教程詳細地介紹了記憶體模型、JVM 架構以及這些術語。

Survivor 區(S0 和 S1):作為年輕代 GC(Minor GC)周期的一部分,存活的對象(仍然被引用的)從 Eden 區被移動到 Survivor 區的 S0 中。類似的,垃圾回收器會掃描 S0 然後将存活的執行個體移動到 S1 中。

(譯注:此處不應該是Eden和S0中存活的都移到S1麼,為什麼會先移到S0再從S0移到S1?)

死亡的執行個體(不再被引用)被标記為垃圾回收。根據垃圾回收器(有四種常用的垃圾回收器,将在下一教程中介紹它們)選擇的不同,要麼被标記的執行個體都會不停地從記憶體中移除,要麼回收過程會在一個單獨的程序中完成。

老年代: 老年代(Old or tenured generation)是堆記憶體中的第二塊邏輯區。當垃圾回收器執行 Minor GC 周期時,在 S1 Survivor 區中的存活執行個體将會被晉升到老年代,而未被引用的對象被标記為回收。

老年代 GC(Major GC):相對于 Java 垃圾回收過程,老年代是執行個體生命周期的最後階段。Major GC 掃描老年代的垃圾回收過程。如果執行個體不再被引用,那麼它們會被标記為回收,否則它們會繼續留在老年代中。

記憶體碎片:一旦執行個體從堆記憶體中被删除,其位置就會變空并且可用于未來執行個體的配置設定。這些空出的空間将會使整個記憶體區域碎片化。為了執行個體的快速配置設定,需要進行碎片整理。基于垃圾回收器的不同選擇,回收的記憶體區域要麼被不停地被整理,要麼在一個單獨的GC程序中完成。

在釋放一個執行個體和回收記憶體空間之前,Java 垃圾回收器會調用執行個體各自的 <code>finalize()</code> 方法,進而該執行個體有機會釋放所持有的資源。雖然可以保證 <code>finalize()</code> 會在回收記憶體空間之前被調用,但是沒有指定的順序和時間。多個執行個體間的順序是無法被預知,甚至可能會并行發生。程式不應該預先調整執行個體之間的順序并使用 <code>finalize()</code> 方法回收資源。

任何在 finalize過程中未被捕獲的異常會自動被忽略,然後該執行個體的 finalize 過程被取消。

JVM 規範中并沒有讨論關于弱引用的垃圾回收機制,也沒有很明确的要求。具體的實作都由實作方決定。

垃圾回收是由一個守護線程完成的。

所有執行個體都沒有活動線程通路。

沒有被其他任何執行個體通路的循環引用執行個體。

Java 中有不同的引用類型。判斷執行個體是否符合垃圾收集的條件都依賴于它的引用類型。

引用類型

垃圾收集

強引用(Strong Reference)

不符合垃圾收集

軟引用(Soft Reference)

垃圾收集可能會執行,但會作為最後的選擇

弱引用(Weak Reference)

符合垃圾收集

虛引用(Phantom Reference)

在編譯過程中作為一種優化技術,Java 編譯器能選擇給執行個體賦 <code>null</code> 值,進而标記執行個體為可回收。

在上面的類中,<code>lion</code> 對象在執行個體化行後從未被使用過。是以 Java 編譯器作為一種優化措施可以直接在執行個體化行後指派<code>lion = null</code>。是以,即使在 SOP 輸出之前, finalize 函數也能夠列印出 <code>'Rest in Peace!'</code>。我們不能證明這确定會發生,因為它依賴JVM的實作方式和運作時使用的記憶體。然而,我們還能學習到一點:如果編譯器看到該執行個體在未來再也不會被引用,能夠選擇并提早釋放執行個體空間。

關于對象什麼時候符合垃圾回收有一個更好的例子。執行個體的所有屬性能被存儲在寄存器中,随後寄存器将被通路并讀取内容。無一例外,這些值将被寫回到執行個體中。雖然這些值在将來能被使用,這個執行個體仍然能被标記為符合垃圾回收。這是一個很經典的例子,不是嗎?

當被指派為null時,這是很簡單的一個符合垃圾回收的示例。當然,複雜的情況可以像上面的幾點。這是由 JVM 實作者所做的選擇。目的是留下盡可能小的記憶體占用,加快響應速度,提高吞吐量。為了實作這一目标, JVM 的實作者可以選擇一個更好的方案或算法在垃圾回收過程中回收記憶體空間。

當 <code>finalize()</code> 方法被調用時,JVM 會釋放該線程上的所有同步鎖。

在判斷哪些記憶體需要回收和什麼時候回收用到GC 算法,本文主要對GC 算法進行講解。

常見的JVM垃圾判定算法包括:引用計數算法、可達性分析算法。

引用計數算法是通過判斷對象的引用數量來決定對象是否可以被回收。

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

優點:簡單,高效,現在的objective-c用的就是這種算法。

缺點:很難處理循環引用,互相引用的兩個對象則無法釋放。是以目前主流的Java虛拟機都摒棄掉了這種算法。

舉個簡單的例子,對象objA和objB都有字段instance,指派令objA.instance=objB及objB.instance=objA,除此之外,這兩個對象沒有任何引用,實際上這兩個對象已經不可能再被通路,但是因為互相引用,導緻它們的引用計數都不為0,是以引用計數算法無法通知GC收集器回收它們。

運作結果

從運作結果看,GC日志中包含“3329K-&gt;744K”,意味着虛拟機并沒有因為這兩個對象互相引用就不回收它們,說明虛拟機不是通過引用技術算法來判斷對象是否存活的。

可達性分析算法是通過判斷對象的引用鍊是否可達來決定對象是否可以被回收。

從GC Roots(每種具體實作對GC Roots有不同的定義)作為起點,向下搜尋它們引用的對象,可以生成一棵引用樹,樹的節點視為可達對象,反之視為不可達。

在Java語言中,可以作為GC Roots的對象包括下面幾種:

虛拟機棧(棧幀中的本地變量表)中的引用對象。

方法區中的類靜态屬性引用的對象。

方法區中的常量引用的對象。

本地方法棧中JNI(Native方法)的引用對象

真正标記以為對象為可回收狀态至少要标記兩次。

強引用就是指在程式代碼之中普遍存在的,類似"Object obj = new Object()"這類的引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

軟引用是用來描述一些還有用但并非必需的對象,對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收範圍進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。在JDK1.2之後,提供了SoftReference類來實作軟引用。

弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象,隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。在JDK1.2之後,提供了WeakReference類來實作弱引用。

虛引用也成為幽靈引用或者幻影引用,它是最弱的一中引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2之後,提供給了PhantomReference類來實作虛引用。

常見的垃圾回收算法包括:标記-清除算法,複制算法,标記-整理算法,分代收集算法。

在介紹JVM垃圾回收算法前,先介紹一個概念。

Stop-the-World

Stop-the-world意味着 JVM由于要執行GC而停止了應用程式的執行,并且這種情形會在任何一種GC算法中發生。當Stop-the-world發生時,除了GC所需的線程以外,所有線程都處于等待狀态直到GC任務完成。事實上,GC優化很多時候就是指減少Stop-the-world發生的時間,進而使系統具有高吞吐 、低停頓的特點。

之是以說标記/清除算法是幾種GC算法中最基礎的算法,是因為後續的收集算法都是基于這種思路并對其不足進行改進而得到的。标記/清除算法的基本思想就跟它的名字一樣,分為“标記”和“清除”兩個階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。

标記階段:标記的過程其實就是前面介紹的可達性分析算法的過程,周遊所有的GC Roots對象,對從GC Roots對象可達的對象都打上一個辨別,一般是在對象的header中,将其記錄為可達對象;

清除階段:清除的過程是對堆記憶體進行周遊,如果發現某個對象沒有被标記為可達對象(通過讀取對象header資訊),則将其回收。

不足:

标記和清除過程效率都不高

會産生大量碎片,記憶體碎片過多可能導緻無法給大對象配置設定記憶體。

将記憶體劃分為大小相等的兩塊,每次隻使用其中一塊,當這一塊記憶體用完了就将還存活的對象複制到另一塊上面,然後再把使用過的記憶體空間進行一次清理。

現在的商業虛拟機都采用這種收集算法來回收新生代,但是并不是将記憶體劃分為大小相等的兩塊,而是分為一塊較大的 Eden 空間和兩塊較小的 Survior 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,将 Eden 和 Survivor 中還存活着的對象一次性複制到另一塊 Survivor 空間上,最後清理 Eden 和 使用過的那一塊 Survivor。HotSpot 虛拟機的 Eden 和 Survivor 的大小比例預設為 8:1,保證了記憶體的使用率達到 90 %。如果每次回收有多于 10% 的對象存活,那麼一塊 Survivor 空間就不夠用了,此時需要依賴于老年代進行配置設定擔保,也就是借用老年代的空間。

将記憶體縮小為原來的一半,浪費了一半的記憶體空間,代價太高;如果不想浪費一半的空間,就需要有額外的空間進行配置設定擔保,以應對被使用的記憶體中所有對象都100%存活的極端情況,是以在老年代一般不能直接選用這種算法。

複制收集算法在對象存活率較高時就要進行較多的複制操作,效率将會變低。

标記—整理算法和标記—清除算法一樣,但是标記—整理算法不是把存活對象複制到另一塊記憶體,而是把存活對象往記憶體的一端移動,然後直接回收邊界以外的記憶體,是以其不會産生記憶體碎片。标記—整理算法提高了記憶體的使用率,并且它适合在收集對象存活時間較長的老年代。

效率不高,不僅要标記存活對象,還要整理所有存活對象的引用位址,在效率上不如複制算法。

分代回收算法實際上是把複制算法和标記整理法的結合,并不是真正一個新的算法,一般分為:老年代(Old Generation)和新生代(Young Generation),老年代就是很少垃圾需要進行回收的,新生代就是有很多的記憶體空間需要回收,是以不同代就采用不同的回收算法,以此來達到高效的回收算法。

新生代:由于新生代産生很多臨時對象,大量對象需要進行回收,是以采用複制算法是最高效的。

老年代:回收的對象很少,都是經過幾次标記後都不是可回收的狀态轉移到老年代的,是以僅有少量對象需要回收,故采用标記清除或者标記整理算法。

&lt;https://segmentfault.com/a/1190000009707894&gt;;

&lt;https://www.cnblogs.com/hysum/p/7100874.html&gt;;

&lt;http://c.biancheng.net/view/939.html&gt;;

&lt;https://www.runoob.com/&gt;;

https://blog.csdn.net/android_hl/article/details/53228348