五、JVM 系列(2) —— GC相關
Java 與 C++ 之間有一堵由 記憶體動態分配和垃圾收集技術 所圍成的 “高牆”,牆外面的人想進去,牆裡面的人卻想出來。
說起 垃圾收集(Garbage Collection, GC),我們需要思考 3件事情。
- 哪些記憶體需要回收?
- 什麼時候回收?
- 如何回收?
1. 哪些記憶體需要回收?
-
程式計數器、 虛擬機器棧 、 本地方法棧 3個區域 隨執行緒而生,隨執行緒而滅。
-
每一個棧幀中分配多少記憶體基本上是在 類結構 確定下來時就已知的。因此這幾個區域的記憶體分配和回收都具有確定性,在這幾個區域內就 不需要過多考慮回收 的問題,因為方法結束或者執行緒結束時,記憶體自然就跟著回收了。
-
而 Java 堆和方法區 不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式 處於執行期間 才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的。即: 垃圾收集器所關注的。
總結:Java 堆和方法區是垃圾收集器所關注的記憶體區域。
2. 什麼時候回收?
在堆裡面存放著 Java 世界中幾乎所有的物件例項,所以哪些物件已經 “死去”( 即不可能再被任何途徑使用的物件 )。就回收哪些。那麼如何判斷物件是否存活呢?
2.1 引用計數演算法(Reference Counting)
- 優點: 簡單、判定效率高的演算法。 給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值 +1;當引用失效時,計數器值 -1;任何時刻計數器為 0 的物件就是不可能再被使用的。
- 缺點:但是主流的 Java 虛擬機器裡面沒有用這個演算法來管理記憶體的。 最主要的原因是它 很難解決物件之間相互迴圈引用的問題。
2.2 可達性分析演算法(Reachability Analysis)
基本思想:通過一系列的稱為 “GC Roots” 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為 引用鏈(Reference Chain) ,當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的。用一個圖能很容易的理解。

可達性分析.png
從圖中知道 GC Roots 的重要性,那麼 Java 中,可作為 GC Roots 的物件包括下面幾種:
- 虛擬機器棧( 棧幀中的區域性變量表 )中引用的物件;
- 方法區中 類靜態屬性 引用的物件;
- 方法區中 常量 引用的物件;
- 本地方法棧中 JNI (即一般說的 Native 方法)引用的物件。
2.3 Java 的四種引用,強軟弱虛
2.3.1 強引用(Strong Reference)
強引用是指在程式碼中普遍存在的,如 Object obj = new Object()
這類的引用,只要強引用還在,GC 永遠不會回收被引用的物件。
2.3.1 軟引用(SoftReference)
用來描述一些 還有用但並非必需的物件 ,對於軟引用關聯的物件, 在將要發生記憶體溢位之前 ,會將這些物件列進回收範圍之中進行第二次回收。 如果這次回收後還是沒有足夠的記憶體,才會丟擲記憶體溢位異常。
2.3.1 弱引用(WeakReference)
用來描述 非必需的物件 。但是它的強度比 軟引用 更弱一點。當 GC 工作時, 無論當前記憶體是否足夠 ,都會回收掉只被弱引用關聯的物件。也就是說被 弱引用關聯的物件 只能生存到下一次 垃圾收集 發生之前。
2.3.1 虛引用(PhantomReference)
也被稱為 幽靈引用或者 幻影引用,最弱的一種引用關係。無法通過 虛引用 來取得一個物件例項。
為一個物件設定 虛引用 關聯的唯一目的就是 能在這個物件被收集器回收時收到一個 系統通知。
2.4 回收方法區
方法區也被稱為 永久代 ,這裡的垃圾收集主要回收兩部分內容: 廢棄常量 和 無用的類
2.4.1 廢棄常量
以常量池中 字面量 的回收為例,假如一個字串 “abc” 已經進入了常量池中,但是當前系統沒有任何一個 String 物件是叫做 “abc” 的,換句話說,就是沒有任何 String 物件引用常量池中的 “abc” 常量。如果這時候發生記憶體回收, 而且必要的話, 這個 “abc” 常量就會被系統清理出常量池。常量池中的其他 類(介面)、方法、欄位的符號引用也與此類似。
2.4.2 無用的類
判定一個類是否是 “無用的類” 的條件則苛刻許多。需同時滿足下面3個條件才能算是 無用的類。
- 該類所有的例項都已經被回收,也就是Java 堆中不存在該類的例項。
- 載入該類的 ClassLoader 已經被回收。
- 該類對應的 java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
當一個類滿足上述3個條件時,虛擬機器 可以 對它進行回收。但也只是 “可以” ,而並不是和物件一樣,不使用了就一定會回收。也就是說虛擬機器也可以不回收它。
是否對類進行回收,HotSpot 虛擬機器提供了 -Xnoclassgc 引數進行控制,
3. 如何回收?
瞭解了 哪些記憶體區域需要回收、四種引用的區別 以及 什麼時候會進行回收,那麼現在要解決的就是 如何進行回收了。即: 垃圾收集演算法 。
3.1 標記-清除演算法(Mark-Sweep)
概述:演算法分為兩個階段 標記 和 清除 兩個階段:首先標記出所有需要回收的物件,在標記完成後統一進行回收。
缺點:
1、效率問題:標記和清除兩個過程的效率都不高;
2、空間問題:標記清除後悔產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次 GC。
3.2 標記-整理演算法(Mark-Compact)
概述:與 標記-清除演算法一樣,但是後續步驟不是直接對 可回收物件 進行清理,而是 讓所有存活的物件都向一端移動,然後直接清理掉 端邊界之外的記憶體,這樣就解決了 記憶體碎片的問題。
3.3 複製演算法(Copying)
概述:將可用的記憶體按容量分為 大小相等 的兩塊,假設為 A塊 和 B塊,每次只使用其中的一塊。當A塊的記憶體用完了,就將還存活的物件複製到 B塊 上面,然後再把 A塊使用過的記憶體空間一次清理掉。
缺點:代價太高,將記憶體縮小為了原來的一半。
改進:現在的商業虛擬機器都採用這種收集演算法來回收 新生代 。 而研究表明,新生代中的物件一般情況下98% 是 朝生夕死 的,所以並不需要按照 1:1 的比例來劃分記憶體空間。
- 將記憶體分為 一塊較大的 Eden 空間 和 兩塊較小的 Survivor 空間 ,每次使用 Eden 和 其中一個 Survivor 。
- 當回收時,將 Eden 和 Survivor 中還存活著的物件一次性複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。
- HotSpot 虛擬機器預設 Eden : Survivor 是 8 : 1, 也就是每次新生代 可用 記憶體空間為 整個 新生代容量的90%(80%+10%)
- 當 Survivor 空間不夠用時,需要依賴 老年代 進行 分配擔保(Handle Promotion)
3.4 分代收集演算法(Generational Collection)
概述:當前商業虛擬機器的 GC 都採用 “分代收集” 演算法。 這種演算法沒有什麼新思想,只是根據物件存活週期的不同將記憶體劃分為幾塊。
一般是將 Java 堆分為 新生代 和 老年代 ,這樣就可以根據每個年代的特點採用最適當的 收集演算法。
- 新生代中,每次 GC 都發現有大批物件死去,只有少量存活,那就使用 複製演算法 。
- 老年代中,物件存活率高,沒有額外空間對它進行分配擔保,就必須使用 標記-清除 或者 標記-整理 演算法進行回收。
參考
《深入理解 Java 虛擬機器》