1. 程式人生 > >垃圾回收與記憶體分配策略

垃圾回收與記憶體分配策略

在瞭解垃圾回收之前,我想問大家三個問題,哪些記憶體需要回收?什麼時候可以回收?怎麼回收?我相信解決了這三個問題大家對GC會有一個更全面的瞭解。

 

哪些記憶體需要回收?

  堆和方法區的記憶體需要被回收。因為程式計數器、虛擬機器棧和本地方法棧3個區域是隨著執行緒而生,隨著執行緒而滅的。棧幀中分配多少記憶體基本上是在類結構確定下來時就已知,因此這幾個區域的記憶體分配和回收都具備確定性。而堆和方法區則不一樣,我們只能在程式執行時才知道會建立哪些物件,這部分的記憶體分配和回收都是動態的。

 

什麼時候回收?

  物件死去的時候。如何判斷物件已經死去呢?

    1.引用計數法:給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1。任何時刻計數器值為0的物件就是不可能再被使用的。由於其很難解決迴圈引用的問題,所以主流的Java虛擬機器沒有選用此種方法管理記憶體。

    2.可達性分析演算法:通過一系列被稱為GC Root的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Root沒有引用鏈的時候,則說明此物件不可用了。

      有哪些物件可以被當成GC Root呢?

      1.虛擬機器棧(棧幀中的本地表量表)中引用的物件,都正在被使用,肯定是活的物件吧

      2.方法區中類靜態屬性所引用的物件,這些是JVM本身需要長期駐留的物件

      3.方法區中常量引用的物件,這些是JVM本身需要長期駐留的物件

      4.本地方法棧中Native方法引用的物件

  注:即使在可達性分析演算法中不可達的物件,他們也不會立刻判為死亡,而是處於死緩階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Root相連線的引用鏈,name他將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()已被虛擬機器呼叫過,虛擬機器將這兩種情況都視為沒有必要執行,然後物件就被判為死亡了。

 

怎麼回收?

  1.標記-清除演算法:演算法分為標記和清除兩個階段,首先標記出需要回收的物件,標記完成後統一回收所有被標記的物件。

    優點:程式設計簡單

    不足:效率低下,標記和清除兩個過程效率都不高;空間問題,清除後會產生大量不連續的記憶體碎片。

 

  2.複製演算法:將記憶體按容量區分為大小相等的兩塊,每次只使用其中一塊。當這一塊記憶體用完了,就將還活著的物件複製到另一塊上,然後把當前使用的這塊記憶體空間一次清理掉。

    優點:實現簡單,執行高效,避免產生大量不連續的記憶體碎片

    不足:可用記憶體縮小為原來的一半,代價高昂。

    優化:新生代中的物件98%都是“朝生夕死”,所以不需要按照1:1的比例來劃分。而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間。每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的物件一次性地複製到另一塊Survivor空間上,最後清理掉Eden和剛才使用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor大小比例是8:1。最後,沒法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時,需要依賴其他記憶體(老年代)來進行分配擔保。

  3.標記-整理演算法:前面過程與標記清除演算法一樣,但是後續步驟不是直接對可回收物件進行清理,而是讓所有存活物件都向一端移動,然後直接清理掉端邊界以外的記憶體。老年代一般採用此種演算法。

  4.分代收集演算法:根據物件存活週期的不同將記憶體劃分為幾塊。一般是吧Java堆分為新生代和老年代,在新生代中每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法。而老年代中因為物件存活率高、沒有額外的空間對他進行擔保,就必須使用標記-清理或者標記-整理演算法來進行回收。

 

  解決了一開始的三大問題之後,我們來看看虛擬機器的記憶體分配與回收策略是什麼。Java技術體系中所提倡的自動記憶體管理最終可以歸結為自動化地解決了兩個問題:給物件分配記憶體以及回收分配給物件的記憶體。

  1.物件優先分配在Eden區:大多數情況下,物件在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次MinorGC(新生代GC)

    例子:JAVA堆大小為20MB,10MB分配給新生代(Eden:Suvivor=8:1),10MB分配給老年代,這時嘗試按順序分配3個2MB和1個4MB大小的物件。

    前3個物件順利分配到Eden,當分配第4個物件時會進行一次MinorGC,因為Eden已經佔了6MB,剩餘空間不足以分配4MB。GC期間虛擬機器又發現已有的3個2MB大小的物件無法全部放入Survivor中,所以之後通過分配擔保機制提前轉移到老年代中。

  2.大物件直接進入老年代

  3.長期存活的物件進入老年代:虛擬機器給每個物件定義了一個年齡計數器,如果在物件在Eden出生並經過第一次MinorGC後仍然存活,並且能被Survior容納的話,將被移動到Survivor空間中,並且物件年齡設為1。物件在Survivor區中沒熬過一次MinorGC,年齡就增加1歲。當他的年齡增加到一定程度(預設15),就會被晉升到老年代中。  

  優化:為了能更好地適應不同程度的記憶體狀況,虛擬機器並不是永遠要求物件的年齡到了設定值才能去老年代,如果在survivor空間中相同年齡所有物件大小的綜合超過了survivor空間的一半,那麼年齡大於等於該年齡的物件就可以直接進入老年代。

  4.空間分配擔保:在發生MinorGC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼MinorGC可以確保是安全的。如果不成立,則虛擬機器會檢視相應設定是否允許擔保失敗。如果允許那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升代老年代物件的平均大小,如果大於,將嘗試進行一次MinorGC,如果小於或者相應設定是不允許冒險,那這時要改為進行一次FullGC(老年代GC)。