這裏介紹的垃圾回收相關算法,主要解決的問題:
判斷哪些內存是垃圾(需要回收的)?
常用的兩種算法:
- 引用計數
- 可達性分析(GC Root)
首先介紹算法前,得定義:
如何判斷一個對象的死亡?
我們一般這樣定義:當一個對象不再被任何存活的對象繼續引用的時候,這個對象就死亡了。
引用計數
引用計數算法,是給每一個對象添加一個計數器,當有對象引用它的時候,計數器+1,當有對象取消對它的引用時,計數就會-1。
當計數器的值為 0 時,即說明沒有對象引用它,也就是這個對象死亡了。
這種算法很簡單,但是有個重大缺陷,那就是無法解決循環引用的問題。
什麼是循環引用問題呢?
比如對象A 引用 對象B,對象B 引用 對象A,那麼 對象A 和 對象B 的計數器都為1。但是如果後續的運行環境再也用不到對象A 和 對象B,那麼就造成了內存泄漏。
上圖就是循環引用的例子。對象引用 Obj1 和 Obj2 在棧中,然後分別指向在堆中的具體實例。然後兩個相互實例中的成員互相引用。那麼對於堆中的對象而言,就有2個引用。一個是來自Obj1,一個來自堆對象的另一方。
如果,現在將 Obj1 指向 nu l l,那麼就如下圖:
這個時候,引用已經不可用了,但是堆中的對象仍然相互引用,他們的計數器不為0,所以無法死亡。
但是,Java 沒有使用這種算法,而是使用了我們後面說的可達性算法,所以接下來的演示,GC 會將這種情況的內存給其清理。
package GC;
public class ReferenceCountGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
// 每個對象中包含2M的成員,方便觀察
private byte[] bigSize = new byte[2 * _1MB];
public static void main(String[] args) {
ReferenceCountGC objA = new ReferenceCountGC();
ReferenceCountGC objB = new ReferenceCountGC();
objA.instance = objB.instance;
objB.instance = objA.instance;
//取消對對象的引用
objA = null;
objB = null;
// 是否進行垃圾回收
System.gc();
}
}
這段代碼實現的就是上面圖片所描述的情況。
首先,我們將 System.gc() 註釋掉,也就是我們在默認情況下,不去觸發垃圾回收。並在運行的時候,添加參數 -XX:+PrintGCDetails。我們觀察輸出結果
可以看到,這個時候,佔用的空間為8M左右。
如果我們取消註釋,也就是主動去調用垃圾回收器,那麼運行結果為:
佔用空間為2M左右。
可以看出來,Java 的垃圾回收,並非採用我們上面介紹的引用計數方式。
可達性分析
可達性算法,還有一系列的別名:根搜索算法,追蹤性垃圾收集,GC Root。
之後,看到原理,其實這些別名都是描述原理的。
首先,我們選取一些對象,這些對象是存活的,也被稱為 GC Roots,然後根據這些對象的引用關係,凡是直接或者間接跟 GC Roots 相關聯的對象,都是存活的。就像圖中的連通性判斷一樣。
這個算法的想法不難。難的是,如何確定 GC Roots。
我們考慮,我們什麼時候需要用到對象?(我們需要對象的時候,肯定需要這個對象是存活的)
- 棧中保存着,我們當前或者之後需要運行的方法及相關參數,所以,棧上所引用的堆中對象肯定是存活的。
- 類中的一些屬性,比如,靜態屬性,因為它不依賴於具體的類
- 一些常用的對象,以免清理后,又要重複加載,比如常用的異常對象,基本數據類型對應的 Class 對象。
除此之外,還有很多零零碎碎的。
在堆結構周圍的一些結構,其中引用的對象可以作為GC Roots
具體 GC Roots 可以概括為:
-
虛擬機棧上(確切的說,是棧幀上的本地變量表)所引用的對象
-
本地方法棧引用的對象
-
方法區中的靜態屬性,常量引用
-
Java 虛擬機的內部引用,常用數據類型的 Class 對象,常駐的異常對象,系統類加載器
-
所有被同步鎖持有的對象
除此之外,還有一些臨時的 GC Roots 可以加入進來。這裏涉及到新生代老年代。
比如老年代中的對象一般都存活時間比較久,也就是大概率是活着的對象,也可臨時作為 GC Roots。
可達性算法的一些細節
前面說了可達性算法,我們根據 GC Roots 來進行標記對象的死活。
但是,被判定為不可達的對象,並不立刻死亡。它仍然有次機會進行自救。
這個自救的機會,是需要重寫 finalize()進行自救。
也就是可達性算法的邏輯大致是這樣的:
- 第一次進行標記,凡是不可達 GC Roots 的對象,都暫時判定為死亡,只是暫時
- 檢查暫時被判定為死亡對象,檢查是否有重寫 finalize()方法,如果有,則觸發,對象可以在裏面完成自救。
如果沒有自救成功 或者 沒有重寫 finalize()方法,則宣告這個對象的死亡。
除此之外,這個對象中的 finalize()方法,只能被調用一次,一生只有一次自救機會。
這個方法,官方並不推薦,所以不必細究。
接下來,演示下上面的兩次標記過程以及自救過程。
(個人認為,《深入理解 Java 虛擬機》中的此章節代碼,略有點不夠完善,故略微改動)
package GC;
import javax.swing.tree.VariableHeightLayoutCache;
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
private byte[] bigSize = new byte[5*1024*1024];
public void isAlive(){
System.out.println("Yes, i am alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalize method executed");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if(SAVE_HOOK != null){
SAVE_HOOK.isAlive();
System.gc();
}else {
System.out.println("Dead");
System.gc();
}
}
}
在這個程序中,我們給這個類,添加名為 bigSize 的屬性,其佔用 4M 大小的空間。
大致分析下代碼邏輯:
- 創建了一個對象,其中有成員佔用 4M 的空間
- 取消對這個對象的引用
- 調用垃圾回收(第一次標記)
- 調用 finalize 方法進行自救
- 之後再次調用垃圾回收(第二次標記)
所以演示的時候,分為兩種情況:
- FinalizeEscapeGC.SAVE_HOOK = this; 未註釋,完成自救
運行時,參數仍然設置為 +XX:PrintGCDetails,可以看到輸出結果:
第一次調用垃圾回收,仍然佔用 5M,說明此時即便失去引用,但是仍然未被清理。
在 finalize()中完成自救后,第二次調用垃圾回收的時候,仍然佔用 5M 的內存大小。說明自救成功。
- FinalizeEscapeGC.SAVE_HOOK = this; 註釋,無法完成自救
第一次垃圾回收,佔用 5M,保留了對象。無法完成自救,然後第二次被清理掉。
所以我發現以下錶述也許更為確切:
- 當對象重寫了 finalize()方法的時候,第一次垃圾回收的時候,如果為不可達對象,對其進行暫緩,並不清理。
- 當對象沒有重寫 finalize()方法的時候,且為不可達對象的時候,直接判定死亡。
本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】
※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!
※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線
※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整
※南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!
※教你寫出一流的銷售文案?
※超省錢租車方案