一、概述
GC
需要考慮的三個問題:
- 哪些記憶體需要回收
- 什麼時候回收
- 如何回收
在分析記憶體區域的時候,我們把Java
執行時資料區分為兩個部分:
- 程式計數器、虛擬機器棧、本地方法棧:每個棧幀中分配多少記憶體在類結構確定下來就已知,因此這些區域的記憶體分配和回收具備確定性,方法結束或執行緒結束時,記憶體就跟著被回收了。
Java
堆、方法區:由於一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也不一樣,只有在程式處於執行期間才能知道會建立哪些物件,因此這些區域的記憶體分配和回收是動態的。
二、如何判斷哪些是“存活”的例項
2.1 引用的分類
引用的定義:如果reference
型別的資料中儲存的數值代表的另外一塊記憶體的起始地址,就稱這塊記憶體代表引用。
引用的分類:
- 強引用(
Object a = new Object()
):只要強引用存在,垃圾回收器永遠不會回收掉被引用的物件。 - 軟引用(
SoftReference
):有用但並非必須,在系統將要發生OOM
異常之前,將會把這些物件列進回收範圍中進行第二次回收。 - 弱引用(
WeakReference
):非必須物件,被弱引用的物件只能生存到下一次垃圾收集發生前。 - 虛引用(
PhantomReference
):不會對生存時間產生影響,也無法通過虛引用來取得一個物件例項,設定虛引用的唯一目的就是能在這個物件被垃圾回收器回收時收到一個系統通知。
2.2 引用計數法
給物件新增一個引用計數器,當有一個地方引用它時就加一,引用失效時就減一,當計數器的值為零時表示它不可用。 但是它無法解決相互迴圈引用問題。
2.3 可達性分析
通過一系列的稱為GC Roots
的物件作為起始點,從這些節點開始向下搜尋,所走過的路徑稱為引用鏈。當一個物件到GC Roots
沒有任何引用鏈時,表示這個物件不可用,GC Roots
的型別有:
- 虛擬機器棧中的區域性變數表中引用的物件。
- 方法區中類靜態屬性引用的物件。
- 方法區中常量引用的物件。
- 本地方法棧中**
JNI
**引用的物件。
2.4 finalize
方法對於記憶體回收的影響
當某個物件在經過可達性分析後,發現它到GC Roots
沒有任何引用鏈時,那麼它會被第一次標記,並進行第一次篩選,篩選的結果有兩種情況:
- 沒有覆蓋
finalize()
方法或者虛擬機器已經呼叫過它的finalize()
方法:直接回收。 - 其它情況:把這個物件放置在一個
F-Queue
的佇列中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer
執行緒去執行這個物件的finalize()
方法,如物件要在finalize
方法中拯救自己,只要重新與引用鏈的某個變數關聯即可,那麼在第二次標記時它將被移出“即將回收”的集合,否則它將被回收。
這種方法代價高昂,不確定性大,無法保證各個物件的呼叫順序,因此可以忘記這個方法的存在。
三、方法區的回收
對於方法區(HotSpot
中的永久代)主要回收兩部分內容:廢棄常量和無用的類。
- 廢棄常量
以常量池中字面量的回收為例,如果一個字串
abc
被放入了常量池中,但是沒有任何一個String
物件引用它,那麼就會被清理出常量池,常量池中其它類(介面)、方法、欄位的符號引用也類似。 - 類 同時滿足三個條件:
- 該類的所有例項已經被回收
- 載入該類的
ClassLoader
已經被回收 - 該類對應的
java.lang.Class
物件沒有在任何地方被引用,無法在任何地方通過發射訪問該類的方法。
四、垃圾收集演算法基礎
4.1 標記 - 清除演算法
- 概念 首先標記出所有需要回收的物件,在標記完成後統一進行回收。
- 缺點:
- 標記和清除兩個過程效率不高。
- 產生記憶體碎片,導致需要分配較大物件時,無法找到足夠的連續記憶體而需要觸發一次
GC
操作。
4.2 複製演算法
-
概念 將可用記憶體劃分為大小相等的兩塊,每次只使用其中的一塊,當一塊記憶體用完了。則觸發一次
GC
操作,將活著的物件複製到另一塊上,然後再把已使用的記憶體空間一次清理掉。 -
缺點 將記憶體縮小為了原來的一半。
-
現在商業虛擬機器採用這種演算法的改良版來實現新生代的回收 它把記憶體按
8:1:1
分為Eden/survivor0/survivor1
三塊: 需要分配記憶體時,首先嚐試在Eden
區分配,如果Eden
區無法分配,那麼嘗試把活著的物件放到survivor0
中去: -
如果
survivor0
可以放入,那麼放入之後清除Eden
區。 -
如果
survivor0
不可以放入,那麼嘗試把Eden
和survivor0
的存活物件放到survivor1
中:- 如果
survivor1
可以放入,那麼放入survivor1
之後清除Eden
和survivor0
,之後再把survivor1
中的物件複製到survivor0
中,保持survivor1
一直為空。 - 如果
survivor1
不可以放入,那麼直接把它們放入到老年代中,並清除Eden
和survivor0
,這個過程也稱為分配擔保。
- 如果
-
適用情況 由於複製演算法在物件成活率較高時,需要較多的複製操作,效率會變低,所以在老年代中不能採用該演算法。
4.3 標記 - 整理演算法
- 概念 和標記 - 清除演算法類似,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。
- 優點 解決了標記- 清除演算法導致的記憶體碎片問題和在存活率較高時複製演算法效率低的問題。
4.4 分代收集演算法
當前商業虛擬機器採用的方式,根據物件存活週期的不同將記憶體劃分為幾塊,一般是新生代和老年代:
- 新生代:每次垃圾收集時只有少量存活,選用複製演算法的改良版,也就是上面說到的
Eden/survivor0/survivor1
的分配方式。 - 老年代:物件存活率較高,且沒有分配擔保,必須用標記 - 清除或標記 - 整理演算法來實現。
五、Minor GC
和Major GC/Full GC
Minor GC
:發生在新生代的垃圾回收動作,非常頻繁,回收速度也較快,採用的垃圾收集器有Serial
、ParNew
、Parallel Scavenge
。Major GC/Full GC
:發生在老年代的GC
,經常伴隨至少一次的Minor GC
,Major GC
的速度一般會比Minor GC
慢十倍以上,採用的垃圾收集器有CMS
、Serial Old
、Parallel Old
。
六、物件分配的原則
-
物件優先在
Eden
區分配 當Eden
區沒有足夠空間,觸發一次Minor GC
。 -
大物件直接進入老年代 例如很長的字串以及陣列,經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來“安置”它們。
-
長期存活的物件將進入老年代 如果
Eden
區出生並進過第一次Minor GC
後,仍然存活,並且被成功複製到survivor
區中,那麼物件年齡變為一,當物件在survivor
中每熬過一次Minor GC
,年齡就增加一,當年齡增加到一定程度,就會晉升到老年代中。 -
動態物件年齡繫結 如果
survivor
空間中相同年齡所有物件大小的總和大於survivor
空間的一半,年齡大於或等於該年齡的物件就可以進入老年代,無須到達要求的年齡。 -
空間分配擔保 在發生
Minor GC
前,檢查老年代最大可用連續空間是否大於新生代所有物件總空間: -
大於,那麼操作是安全的,不對老年代進行
Full GC
。 -
小於,檢查
HandlePromotionFailure
設定值是否允許失敗:- 允許:檢查老年代最大可用連續空間是否大於歷次晉升到老年代物件的平均大小:
- 大於:不對老年代進行
Full GC
。在這之後,因為有可能出現某次存活物件激增的情況,這種屬於冒險行為,如果出現了擔保失敗(也就是Eden
和survivor0
的存活物件既無法放入survivor1
,也無法放入老年代的連續空間中),那麼會在失敗之後對老年代進行Full GC
。 - 小於:先對老年代進行一次
Full GC
。
- 大於:不對老年代進行
- 不允許:先對老年代執行一次
Full GC
。
- 允許:檢查老年代最大可用連續空間是否大於歷次晉升到老年代物件的平均大小: