Java垃圾回收器與內存分配策略


概述

說起垃圾收集器(Garbage Collection,GC),大部分人都把這項技術當做Java的伴生產物。實際上GC的歷史遠比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當Lisp還在胚胎時期時,人們就在思考GC需要完成的3件事情:

  • 那些內存需要回收?
  • 什麼時候回收內存?
  • 如何回收?

垃圾收集器關注那些數據區域

  • 程序計數器
  • 虛擬機棧
  • 本地方法棧
  • 方法區

程序計數器、Java虛擬機棧、本地方法棧這3個區域都是隨線程而生,隨線程而滅;棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每個棧幀分配多少內存基本上是在類結構確定下來的時候就已知的,因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,以爲方法結束或者線程結束時,內存自然就跟隨着回收了。而Java堆區和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間才能知道會創建那些對象,這部分內存的分配和回收都是動態的,垃圾回收器關注的是這部分內存。

先講述Java堆區中的對象回收。

判斷對象是否存活

  • 引用計數:通過判斷對象被引用的次數(爲0,則表示不可被使用),但這很難解決對象相互循環引用的問題。
  • 根搜索算法:即採用有向圖的方式,判斷從GC Roots到某個對象是否可達。

GC-可達性分析.png

什麼樣的對象能作爲GC的Root節點呢?

  • 虛擬機棧中局部變量引用的對象
  • 類靜態屬性引用的對象
  • 常量引用的對象
  • JNI中引用的對象

對象的回收

要宣告一個對象死亡,只少要經歷兩次標記過程:如果對象在進行可達行分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被進行一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲“沒有必要執行”。

對象回收.png

對象的引用類型

說起對象的回收我們就不能不說對象的引用了,因爲無論【引用計數法】判斷對象的引用數量,或者【根搜索算法】判斷對象的應用鏈是否可達,判定對象是否存活都與引用有關。在JDK1.2之後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)與虛引用(Phantom Reference)四種,這四中引用程序依次逐漸減弱。

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

方法區的回收

方法區或者是HotSpot虛擬機的永久代的垃圾回收主要回收的內容有兩部分:廢棄的常量和無用的類。

廢棄的常量回收和Java堆中的對象回收時類似的。

判斷一個類是否是【無用的類】卻比判斷一個對象是否被可以被回收苛刻的多,該類需要滿足同時滿足一下三個條件:

  • 改類的所有實力都以及被回收,也就是說Java堆中存在改類的任何實力;
  • 加載該類的ClassLoader都以及被回收。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

垃圾回收算法

  • 標記-清除算法(Mark-Sweep)
  • 複製算法(Copying)
  • 標記-整理算法(Mark-Compact)

標記-清除算法(Mark-Sweep)

【標記-清除】是最基礎的收集算法,算法分爲“標記”和“清除”兩個階段,首先標記處所有需要回收的對象,在標記完成後統一回收所被標記的對象,它的標記過程就是上邊講的對象的回收中的標記。
特點:

  • 標記和清除效率都不高
  • 標記清除後會產生大量內存碎片

標記-清除

複製算法(Copying)

爲了解決效率問題,一種稱爲“複製”的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊,當這一塊的內存用完了,就將其存活着的對象複製到另外一塊上面,然後再把已使用過的內存一次清理掉。

  • 不會產生碎片
  • 運行效率高
  • 內存縮小了一半

複製算法

標記-整理算法(Mark Compact)

標記-整理算法是介於【標記-清除】和【複製】之間的收集算法,標記過程任然與【標記-清除】算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

標記-整理

分代收集算法(Generational Collection)

當前商業虛擬機的垃圾收集都是採用“分代收集”(Generational Collection)算法,這種算法並沒有什麼新的思想,只是根據對象存活週期的不同將內存劃分爲幾塊。一般是把Java堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次來及收集時都發現有大批對象死去,只有少量存活,那就選用複製算犯法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高,沒有額外控件對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。

內存分配策略

Java內存分配

這裏所說的內存分配,主要至的是在堆上的分配,一半的,對象的內存分配都是在堆上進行,但現代技術頁支持將對象拆程標量類型(標量類型即原子類型,表示單個值,可以是基本類型或String類型),然後在棧上分配,在棧上分配很少見,我們這裏不考慮。

Java內存分配和回收的機制概括的說,就是分代分配,分代回收。對象根據存活的時間被分爲:年輕代(Young Generation)、老年代(Old Generation)、永久代(Permanent Generation,也就是方法區)。

年輕代(Young Generation):對象被創建時,內存的分配首先發生在年輕代(大對象可以直接 被創建在年老代),大部分的對象在創建後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消 亡的),這個GC機制被稱爲Minor GC或叫Young GC。注意,Minor GC並不代表年輕代內存不足,它事實上只表示在Eden區上的GC。

Minor GC:採用複製算法(Copying)

年老代(Old Generation):對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次 Young GC後存活了下來),則會被複制到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代內存不足時, 將執行Major GC,也叫 Full GC。 

Full GC:標記-整理算法(Mark-Compact)

GC.png

年輕代上的內存分配是這樣的,年輕代可以分爲3個區域:Eden區(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示內存首次分配的區域,再 貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。

絕大多數剛創建的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的內存空間,因此在其上分配內存極快;

當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩餘的對象複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);

此後,每次Eden區滿了,就執行一次Minor GC,並將剩餘的對象都添加到Survivor0;

當Survivor0也滿的時候,將其中仍然活着的對象直接複製到Survivor1,以後Eden區執行Minor GC後,就將剩餘的對象添加Survivor1(此時,Survivor0是空白的)。

當兩個存活區切換了幾次(HotSpot虛擬機默認15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代)之後,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被複制到老年代。

對象優先在Eden區分配

大對象直接進入老年代

長期存活的對象將進入老年代

第一次進入Survivor區域的時候對象年齡設置爲1,對象在Survivor區域中每“熬過”一次MinorGC,年齡增加一歲,當它的年齡增加到一定程度(默認爲15歲),將會被晉升到老年代中。

動態對象年齡判斷

爲了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

在發生MinorGC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新手代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將會嘗試着一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure設置不允許冒險(冒險:當出現大量對象在Minor GC後任然存活的情況,就需要老年代進行分配擔保 ,把Survivor無法容納的對象直接進入老年代),那這時改爲進行一次Full GC。


摘抄至原文: http://blog.csdn.net/stven_king/article/details/77602255

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