垃圾收集是一項自動化的技術。可是當我們排查各種內存問題,或者當垃圾收集成爲系統達到更高併發量的瓶頸時,我們需要對這些原本自動化的技術進行必要的監控和調節,所有我們很有必要學習 JVM 的垃圾收集機制。
一. 什麼區域需要回收?爲什麼需要回收?
垃圾回收也稱爲 GC (Garbage Collection),或者可以稱爲垃圾收集。
對於線程私有的三個部分(程序計數器,虛擬機棧和本地方法棧),不怎麼需要考慮回收問題,原因:
- 在方法結束或線程結束時,內存便跟着回收走了,他們隨線程而生,線程而滅
- 而且對於棧來說,每個棧幀中分配多少內存基本在類結構確定下來的時候就已經確定了。
對於線程共享的兩個部分(堆和方法區,主要是堆),需要考慮回收,原因:
- 程序只有處於運行的時候才能知道會創建哪些對象
- 內存的分配和回收都是動態的
對於堆來說,如果不進行垃圾回收,內存遲早都會被消耗空,因爲我們在不斷的分配內存空間而不進行回收。除非內存無限大,我們可以任性的分配而不回收,但是事實並非如此。所以,垃圾回收是必須的。
二. 如何判斷對象是否存活?
在對堆進行垃圾回收前,必須確定每個對象是否還存活着;而這個判斷過程主要是以下兩種算法
1. 引用計數算法
給對象添加一個引用計數器,每當有一個地方引用它,計數器值加 1;每當引用失效,計數器減 1;當某個對象任何時候計數器值都是 0 時,這個對象就“死”了
缺點:很難解決對象之間循環引用的問題,也因此主流的 JVM 都沒有使用該算法來管理內存
public class GCTest {
Object object;
private static void test() {
GCTest test1 = new GCTest();
GCTest test2 = new GCTest();
test1.object = test2;
test2.object = test1;
test1 = null;
test2 = null;
}
}
類似這樣的例子,由於 test1 和 test2 相互引用對方,即使這兩個對象已經不可能再被訪問到(兩個變量都已經指向 null),引用計數算法也無法讓垃圾收集器對它們進行回收
2. 可達性分析算法
這是主流 JVM 使用的回收算法
通過一系列稱爲 GC Roots 的對象作爲起始點,從這些節點向下搜索,如果一個對象與 GC Roots 對象有引用鏈相連,說明對象可用;反之,對象不可用
如上圖的對象 4,5,6 則需要被回收
而可作爲 GC Roots 的對象包括下面幾種:
- 虛擬機棧中引用的對象
- 方法區中類靜態屬性引用的對象以及常量引用的對象
- 本地方法棧中 native 方法引用的對象
三. 方法區的回收
方法區很少進行垃圾回收,甚至可以不要求虛擬機對方法區進行回收,因爲能回收的東西很少,因此也叫做永久代
在永久代,主要回收兩個內容:廢棄常量,無用的類。如果在永久代發生垃圾回收,那麼這兩個內容就會被清理出去(當然大多數情況下不會去對永久代進行垃圾回收操作)
四. 引用的類型
不論使用什麼算法判斷對象的存活情況,這都和“引用”息息相關
1. 強引用
- 簡單來說就是類似
Object o = new Objrct
這樣的引用 - 只要這樣的關係還存在,就永不會被回收
2. 軟引用
- 還有用但非必需的對象
- 如果將要發生內存溢出,則進行第二次回收,將這些軟引用對象回收;之後如果還是沒有足夠的內存,再拋出內存溢出異常
可以通過 SoftReference 實現,這是 sf 對 obj 有軟應用
Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
sf.get(); //如果 obj 被標記爲需要被回收,則會返回null
SoftReference 可以用來實現類似緩存的功能
3. 弱引用
- 非必需對象,比軟引用強度更弱
- 當垃圾收集器工作,它們就會被回收
可以通過 WeakReference 實現,通常用於監控對象是否已經被垃圾回收器標記爲即將回收的垃圾
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
wf.get();
wf.isEnQueued();//返回是否被垃圾回收器標記爲即將回收的垃圾
4. 虛引用
- 最弱的引用關係
- 無法通過虛引用取得一個對象的實例
可以通過 PhantomReference 實現,主要用於檢測對象是否已經從內存中刪除
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
pf.get();//永遠返回null
pf.isEnQueued();//返回是否從內存中已經刪除
五. 垃圾回收算法
1. 標記——清除算法
先標記出要回收的對象,標記完成後統一清除這些對象。
缺點:
- 效率太低,標記和清除兩個操作的效率都不高
- 清除後會產生大量不連續的內存空間,或者稱爲內存碎片。而如果我們需要分配一些較大的對象的時候,無法找到足夠的連續空間是一件很麻煩的事情。
2. 複製算法
將內存劃分爲等大的兩塊,一次只使用一塊。當其中一塊用完了,就把裏面的存活的對象全部複製到另一塊去,然後將已經使用的那一大塊一次性全部清理掉
- 優點:實現簡單,運行高效,也不用擔心碎片問題
- 缺點:將內存縮小了一半,代價有點高
3. 標記——整理算法
標記——清除算法的改進,在完成標記之後,讓所有存活的對象都向一端移動,然後直接清理掉邊界以外的內存,故名叫“整理”。
4. 分代收集算法
當前商業虛擬機的垃圾收集都採用「分代收集」算法
分代收集算法將內存劃分爲新生代和老年代:
在新生代中,每次垃圾收集都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集
在老年代中因爲對象存活率高、沒有額外空間對它進行擔保,就必須採用「標記 — 清理」或者「標記 — 整理」算法來回收。
猜你喜歡
- 算法必學:經典的 Top K 問題
- Java 程序員都該懂的 volatile 關鍵字
- 談一談 JVM 對鎖的優化
- 教你 Shiro + SpringBoot 整合 JWT
- 教你 Shiro 整合 SpringBoot,避開各種坑
- 你必須搞清楚的String,StringBuilder,StringBuffer
- 分享一些 Java 後端的個人乾貨