天天看點

Java垃圾回收器與記憶體配置設定政策

上一篇JVM記憶體模型講述了Java虛拟機在運作時所管理的記憶體劃分下的每個資料區域的各自用途,以及建立和銷毀時間。當需要排查各種記憶體洩漏、記憶體溢出問題時,當來及收內建為系統達到更高并發量的瓶頸時,我們需要對JVM的GC機制和記憶體配置設定又更多的了解,這邊文章是在上一篇文章的基礎之上講述了Java垃圾回收器與記憶體配置設定政策。

概述

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

  • 那些記憶體需要回收?
  • 什麼時候回收記憶體?
  • 如何回收?

垃圾收集器關注那些資料區域

  • 程式計數器
  • 虛拟機棧
  • 本地方法棧
  • 方法區

程式計數器、Java虛拟機棧、本地方法棧這3個區域都是随線程而生,随線程而滅;棧中的棧幀随着方法的進入和退出而有條不紊地執行着出棧和入棧操作。每個棧幀配置設定多少記憶體基本上是在類結構确定下來的時候就已知的,是以這幾個區域的記憶體配置設定和回收都具備确定性,在這幾個區域内就不需要過多考慮回收的問題,以為方法結束或者線程結束時,記憶體自然就跟随着回收了。而Java堆區和方法區則不一樣,一個接口中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們隻有在程式處于運作期間才能知道會建立那些對象,這部分記憶體的配置設定和回收都是動态的,垃圾回收器關注的是這部分記憶體。

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

判斷對象是否存活

  • 引用計數:通過判斷對象被引用的次數(為0,則表示不可被使用),但這很難解決對象互相循環引用的問題。
  • 根搜尋算法:即采用有向圖的方式,判斷從GC Roots到某個對象是否可達。
Java垃圾回收器與記憶體配置設定政策

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

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

對象的回收

要宣告一個對象死亡,隻少要經曆兩次标記過程:如果對象在進行可達行分析後發現沒有與GC Roots相連結的引用鍊,那它将會被進行一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法,或者finalize()方法已經被虛拟機調用過,虛拟機将這兩種情況都視為“沒有必要執行”。

Java垃圾回收器與記憶體配置設定政策

對象的引用類型

說起對象的回收我們就不能不說對象的引用了,因為無論【引用計數法】判斷對象的引用數量,或者【根搜尋算法】判斷對象的應用鍊是否可達,判定對象是否存活都與引用有關。在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)

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

特點:

  • 标記和清除效率都不高
  • 标記清除後會産生大量記憶體碎片
Java垃圾回收器與記憶體配置設定政策

複制算法(Copying)

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

  • 不會産生碎片
  • 運作效率高
  • 記憶體縮小了一半
Java垃圾回收器與記憶體配置設定政策

标記-整理算法(Mark Compact)

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

Java垃圾回收器與記憶體配置設定政策

分代收集算法(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)

Java垃圾回收器與記憶體配置設定政策
年輕代上的記憶體配置設定是這樣的,年輕代可以分為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。

文章到這裡就全部講述完啦,若有其他需要交流的可以留言哦!!

繼續閱讀