Java的內存分配與回收所有由JVM垃圾回收進程自動完成。與C語言不一樣,Java開發者不須要本身編寫代碼實現垃圾回收。這是Java深受你們歡迎的衆多特性之一,可以幫助程序員更好地編寫Java程序。html
下面四篇教程是瞭解Java 垃圾回收(GC)的基礎:java
這篇教程是系列第一部分。首先會解釋基本的術語,好比JDK、JVM、JRE和HotSpotVM。接着會介紹JVM結構和Java 堆內存結構。理解這些基礎對於理解後面的垃圾回收知識很重要。程序員
每種JVM實現可能採用不一樣的方法實現垃圾回收機制。在收購SUN以前,Oracle使用的是JRockit JVM,收購以後使用HotSpot JVM。目前Oracle擁有兩種JVM實現而且一段時間後兩個JVM實現會合二爲一。面試
HotSpot JVM是目前Oracle SE平臺標準核心組件的一部分。在這篇垃圾回收教程中,咱們將會了解基於HotSpot虛擬機的垃圾回收原則。objective-c
下面圖片總結了JVM的關鍵組件。在JVM體系結構中,與垃圾回收相關的兩個主要組件是堆內存和垃圾回收器。堆內存是內存數據區,用來保存運行時的對象實例。垃圾回收器也會在這裏操做。如今咱們知道這些組件是如何在框架中工做的。算法
咱們有必要了解堆內存在JVM內存模型的角色。在運行時,Java的實例被存放在堆內存區域。當一個對象再也不被引用時,知足條件就會從堆內存移除。在垃圾回收進程中,這些對象將會從堆內存移除而且內存空間被回收。堆內存如下三個主要區域:數據庫
永久代空間在Java SE8特性中已經被移除。數組
Java 垃圾回收是一項自動化的過程,用來管理程序所使用的運行時內存。經過這一自動化過程,JVM 解除了程序員在程序中分配和釋放內存資源的開銷。安全
做爲一個自動的過程,程序員不須要在代碼中顯示地啓動垃圾回收過程。System.gc()
和Runtime.gc()
用來請求JVM啓動垃圾回收。微信
雖然這個請求機制提供給程序員一個啓動 GC 過程的機會,可是啓動由 JVM負責。JVM能夠拒絕這個請求,因此並不保證這些調用都將執行垃圾回收。啓動時機的選擇由JVM決定,而且取決於堆內存中Eden區是否可用。JVM將這個選擇留給了Java規範的實現,不一樣實現具體使用的算法不盡相同。
毋庸置疑,咱們知道垃圾回收過程是不能被強制執行的。我剛剛發現了一個調用System.gc()
有意義的場景。經過這篇文章瞭解一下適合調用System.gc() 這種極端狀況。
說到GC類型,就更有意思了,爲何呢,由於業界沒有統一的嚴格意義上的界限,也沒有嚴格意義上的GC類型,都是左邊一個教授一套名字,右邊一個做者一套名字。爲何會有這個狀況呢,由於GC類型是和收集器有關的,不一樣的收集器會有本身獨特的一些收集類型。因此做者在這裏引用R大關於GC類型的介紹,做者以爲仍是比較穩當準確的。以下:
觸發時機
上面你們也看到了,GC類型分分類是和收集器有關的,那麼固然了,對於不一樣的收集器,GC觸發時機也是不同的,做者就針對默認的serial 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可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
什麼是Stop the world
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 垃圾回收器會調用實例各自的 finalize()
方法,從而該實例有機會釋放所持有的資源。雖然能夠保證 finalize()
會在回收內存空間以前被調用,可是沒有指定的順序和時間。多個實例間的順序是沒法被預知,甚至可能會並行發生。程序不該該預先調整實例之間的順序並使用 finalize()
方法回收資源。
Java 中有不一樣的引用類型。判斷實例是否符合垃圾收集的條件都依賴於它的引用類型。
引用類型 | 垃圾收集 |
---|---|
強引用(Strong Reference) | 不符合垃圾收集 |
軟引用(Soft Reference) | 垃圾收集可能會執行,但會做爲最後的選擇 |
弱引用(Weak Reference) | 符合垃圾收集 |
虛引用(Phantom Reference) | 符合垃圾收集 |
在編譯過程當中做爲一種優化技術,Java 編譯器能選擇給實例賦 null
值,從而標記實例爲可回收。
1 2 3 4 5 6 7 8 9 10 |
|
在上面的類中,lion
對象在實例化行後從未被使用過。所以 Java 編譯器做爲一種優化措施能夠直接在實例化行後賦值lion = null
。所以,即便在 SOP 輸出以前, finalize 函數也可以打印出 'Rest in Peace!'
。咱們不能證實這肯定會發生,由於它依賴JVM的實現方式和運行時使用的內存。然而,咱們還能學習到一點:若是編譯器看到該實例在將來不再會被引用,可以選擇並提前釋放實例空間。
finalize()
方法被調用時,JVM 會釋放該線程上的全部同步鎖。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
在判斷哪些內存須要回收和何時回收用到GC 算法,本文主要對GC 算法進行講解。
常見的JVM垃圾斷定算法包括:引用計數算法、可達性分析算法。
引用計數算法是經過判斷對象的引用數量來決定對象是否能夠被回收。
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。
優勢:簡單,高效,如今的objective-c用的就是這種算法。
缺點:很難處理循環引用,相互引用的兩個對象則沒法釋放。所以目前主流的Java虛擬機都摒棄掉了這種算法。
舉個簡單的例子,對象objA和objB都有字段instance,賦值令objA.instance=objB及objB.instance=objA,除此以外,這兩個對象沒有任何引用,實際上這兩個對象已經不可能再被訪問,可是由於互相引用,致使它們的引用計數都不爲0,所以引用計數算法沒法通知GC收集器回收它們。
public class ReferenceCountingGC { public Object instance = null; public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc();//GC } }
public class ReferenceCountingGC { public Object instance = null; public static void main(String[] args) { ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc();//GC } }
運行結果
[GC (System.gc()) [PSYoungGen: 3329K->744K(38400K)] 3329K->752K(125952K), 0.0341414 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] [Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->628K(87552K)] 752K->628K(125952K), [Metaspace: 3450K->3450K(1056768K)], 0.0060728 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] Heap PSYoungGen total 38400K, used 998K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000) eden space 33280K, 3% used [0x00000000d5c00000,0x00000000d5cf9b20,0x00000000d7c80000) from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000) to space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000) ParOldGen total 87552K, used 628K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000) object space 87552K, 0% used [0x0000000081400000,0x000000008149d2c8,0x0000000086980000) Metaspace used 3469K, capacity 4496K, committed 4864K, reserved 1056768K class space used 381K, capacity 388K, committed 512K, reserved 1048576K Process finished with exit code 0
[GC (System.gc()) [PSYoungGen: 3329K->744K(38400K)] 3329K->752K(125952K), 0.0341414 secs] [Times: user=0.00 sys=0.00, real=0.06 secs] [Full GC (System.gc()) [PSYoungGen: 744K->0K(38400K)] [ParOldGen: 8K->628K(87552K)] 752K->628K(125952K), [Metaspace: 3450K->3450K(1056768K)], 0.0060728 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] Heap PSYoungGen total 38400K, used 998K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000) eden space 33280K, 3% used [0x00000000d5c00000,0x00000000d5cf9b20,0x00000000d7c80000) from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000) to space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000) ParOldGen total 87552K, used 628K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000) object space 87552K, 0% used [0x0000000081400000,0x000000008149d2c8,0x0000000086980000) Metaspace used 3469K, capacity 4496K, committed 4864K, reserved 1056768K class space used 381K, capacity 388K, committed 512K, reserved 1048576K Process finished with exit code 0
從運行結果看,GC日誌中包含「3329K->744K」,意味着虛擬機並無由於這兩個對象互相引用就不回收它們,說明虛擬機不是經過引用技術算法來判斷對象是否存活的。
可達性分析算法是經過判斷對象的引用鏈是否可達來決定對象是否能夠被回收。
從GC Roots(每種具體實現對GC Roots有不一樣的定義)做爲起點,向下搜索它們引用的對象,能夠生成一棵引用樹,樹的節點視爲可達對象,反之視爲不可達。
在Java語言中,能夠做爲GC Roots的對象包括下面幾種:
真正標記覺得對象爲可回收狀態至少要標記兩次。
強引用就是指在程序代碼之中廣泛存在的,相似"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
Object obj = new Object();
Object obj = new Object();
軟引用是用來描述一些還有用但並不是必需的對象,對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。在JDK1.2以後,提供了SoftReference類來實現軟引用。
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj);
Object obj = new Object(); SoftReference<Object> sf = new SoftReference<Object>(obj);
弱引用也是用來描述非必需對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象,只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在JDK1.2以後,提供了WeakReference類來實現弱引用。
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj);
Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj);
虛引用也成爲幽靈引用或者幻影引用,它是最弱的一中引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知。在JDK1.2以後,提供給了PhantomReference類來實現虛引用。
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj);
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj);
常見的垃圾回收算法包括:標記-清除算法,複製算法,標記-整理算法,分代收集算法。
在介紹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 空間就不夠用了,此時須要依賴於老年代進行分配擔保,也就是借用老年代的空間。
不足:
標記—整理算法和標記—清除算法同樣,可是標記—整理算法不是把存活對象複製到另外一塊內存,而是把存活對象往內存的一端移動,而後直接回收邊界之外的內存,所以其不會產生內存碎片。標記—整理算法提升了內存的利用率,而且它適合在收集對象存活時間較長的老年代。
不足:
效率不高,不只要標記存活對象,還要整理全部存活對象的引用地址,在效率上不如複製算法。
分代回收算法其實是把複製算法和標記整理法的結合,並非真正一個新的算法,通常分爲:老年代(Old Generation)和新生代(Young Generation),老年代就是不多垃圾須要進行回收的,新生代就是有不少的內存空間須要回收,因此不一樣代就採用不一樣的回收算法,以此來達到高效的回收算法。
新生代:因爲新生代產生不少臨時對象,大量對象須要進行回收,因此採用複製算法是最高效的。
老年代:回收的對象不多,都是通過幾回標記後都不是可回收的狀態轉移到老年代的,因此僅有少許對象須要回收,故採用標記清除或者標記整理算法。
微信公衆號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公衆號後回覆」Java「便可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分佈式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送做者原創的Java學習指南、Java程序員面試指南等乾貨資源)