讀書筆記《深入理解Java虛擬機器》 (三)物件已死?與記憶體分配策略
物件是否可回收
- 引用計數演算法
給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時就減1;當等於0時就認為物件不可能再被使用。問題:當兩個物件相互引用時,就無法回收了。
- 可達性分析演算法
通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連,則證明物件是不可用的。
可作為GCRoots的物件包括下面幾種:
- 虛擬機器棧(棧幀中的本地變量表)中引用的物件。
- 方法區中類靜態屬性引用的物件。
- 方法區中常量引用的物件。
- 本地方法棧中JNI(Native方法)引用的物件。
引用的種類
- 強引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件,類似“Object obj = new Object()”
- 軟引用,SoftReference,描述一些有用,但是非必需的物件,在系統將要發生記憶體溢位異常前會將此類引用列入回收範圍進行第二次回收。
- 弱引用,WeekReference,被弱引用關聯的物件只能生存到下一次垃圾收集,無論當前記憶體是否足夠。
- 虛引用,PhantomReference,無法通過虛引用來取得一個物件例項。為一個物件設定虛引用的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
物件的自我拯救
重寫finalize()方法,只能執行一次。
方法區的回收
垃圾收集率低, 常量池中的無用常量、無用類就算達到了回收條件也未必一定會被回收。
垃圾收集演算法
- 標記 — 清除演算法
首先標記出所有需要回收的物件,在標記完成後統一回收。不足:效率低;會產生大量不連續的記憶體碎片。空間碎片太多可能會導致以後在程式執行過程中分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
- 複製演算法
將記憶體塊劃分為容量大小相同的兩塊,每次只使用其中的一塊,當這塊記憶體用完時,就將這塊記憶體中還存活的物件複製到另外一塊記憶體中,並將當前記憶體塊清理掉。實現簡單,執行高效。但是記憶體縮小一半。
- 標記 — 整理演算法
首先標記出所有需要回收的物件, 然後把還存活的物件向一端移動,然後清理存活物件邊界以外的空間。
- 分代收集演算法
根據物件存活週期的不同將記憶體劃分為幾塊。 一般分為新生代和老年代。新生代每次垃圾回收都會有大批物件死去,所以選用複製演算法,存活物件少, 複製成本低。老年代中物件存活率高,沒有額外的空間對它進行分配擔保,所以就使用“標記 —清理”或者“標記 — 整理”演算法來進行回收。
記憶體分配與回收策略
- 物件優先在Eden分配
大多數情況下,物件再新生代Eden區中分配。當Eden區中沒有足夠空間時,虛擬機器會出發一次Minor GC(新生代GC)。
- 大物件直接進入老年代
大物件是指需要大量連續記憶體空間的Java物件,比如很長的字串以及陣列。抵制String用加號多次拼接吧。大物件大多的話, 會增加提前觸發垃圾收集的機率。
- 長期存活的物件將進入老年代
虛擬機器給每個物件定義了個年齡計數器,每經過一次新生代GC(Minor GC)還存活的話, 那年齡計數器就加1歲,直到增加到一定程度(預設為15歲),就會移動到老年代中。
- 動態物件年齡判定
如果在Survivor空間中相同年齡所有物件的大小總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。
空間分配擔保
在發生新生代GC之前,虛擬機器會先檢查老年代最大可用空間是否大於新生代物件的總空間,如果不大於,則擔保失敗。如果允許擔保失敗,那麼新生代GC有可能會失敗,因為新生代GC需要複製出來的存活物件不能確定大小。當不允許擔保失敗或者新生代GC失敗,會觸發Full GC。一般是允許擔保失敗,來減少Full GC的次數。