1. 程式人生 > >我說gc你說喲 (一)

我說gc你說喲 (一)

雖然我還沒有被問到,但是我肯定會被問到,來自掙扎線上苦苦掙扎的阿狗一個,暴風哭泣~.~

-----------------Java堆記憶體

Java堆是被所有執行緒共享的一塊記憶體區域,所有物件例項和陣列都在堆上進行記憶體分配。為了進行高效的垃圾回收,虛擬機器把堆記憶體劃分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3個區域。

新生代

新生代由 Eden Survivor Space(From Space,To Space)構成,大小通過-Xmn引數指定,Eden 與 Survivor Space 的記憶體大小比例預設為8:1,可以通過-XX:SurvivorRatio 引數指定,比如新生代為10M 時,Eden分配8M,S0和S1各分配1M。

物件主要分配在新生代的Eden SpaceFrom Space,少數情況下會直接分配在老年代。如果新生代的Eden Space和From Space的空間不足,則會發起一次minor GC,在GC的過程中,會將Eden Space和From  Space中的存活物件移動到To Space,然後將Eden Space和From Space進行清理。如果在清理的過程中,To Space無法足夠來儲存某個物件,就會將該物件移動到老年代中。在進行了GC之後,使用的便是Eden space和To Space了,下次GC時會將存活物件複製到From Space,如此反覆迴圈。當物件在Survivor區躲過一次GC的話,其物件年齡便會加1,預設情況下,如果物件年齡達到15歲,就會移動到老年代中。

老年代

老年代的空間大小即-Xmx 與-Xmn 兩個引數之差,用於存放經過幾次Minor GC之後依舊存活的物件。當老年代的空間不足時,會觸發Major GC/Full GC,速度一般比Minor GC慢10倍以上。

永久代

在JDK8之前的HotSpot實現中,類的元資料如方法資料、方法資訊(位元組碼,棧和變數大小)、執行時常量池、已確定的符號引用和虛方法表等被儲存在永久代中,32位預設永久代的大小為64M,64位預設為85M,可以通過引數-XX:MaxPermSize進行設定,一旦類的元資料超過了永久代大小,就會丟擲OOM異常。

---------------如何確定某個物件是“垃圾”?(引用計數法
可達性分析法。)

在java中是通過引用來和物件進行關聯的,也就是說如果要操作物件,必須通過引用來進行。那麼很顯然一個簡單的辦法就是通過引用計數來判斷一個物件是否可以被回收。特點是實現簡單,效率較高,但是它無法解決迴圈引用的問題。

public class GCtest {
    private Object instance = null;
    private static final int _10M = 10 * 1 << 20;
    // 一個物件佔10M,方便在GC日誌中看出是否被回收
    private byte[] bigSize = new byte[_10M];
 
    public static void main(String[] args) {
        GCtest objA = new GCtest();
        GCtest objB = new GCtest();
 
        objA.instance = objB;
        objB.instance = objA;
 
        objA = null;
        objB = null;
 
        System.gc();
    }
}

為了解決這個問題,在Java中採取了 可達性分析法。該方法的基本思想是通過一系列的“GC Roots”物件作為起點進行搜尋,判斷在“GC Roots”和一個物件之間有沒有可達路徑。

以下物件可作為GC Roots:

  • 本地變量表中引用的物件
  • 方法區中靜態變數引用的物件
  • 方法區中常量引用的物件
  • Native方法引用的物件

當一個物件到 GC Roots 沒有任何引用鏈時,意味著該物件可以被回收。

 在可達性分析法中,判定一個物件objA是否可回收,至少要經歷兩次標記過程: 1、如果物件objA到 GC Roots沒有引用鏈,則進行第一次標記。 2、如果物件objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F-Queue佇列中,由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒觸發其finalize()方法。finalize()方法是物件逃脫死亡的最後機會,GC會對佇列中的物件進行第二次標記,如果objA在finalize()方法中與引用鏈上的任何一個物件建立聯絡,那麼在第二次標記時,objA會被移出“即將回收”集合。

public class FinalizerTest {
    public static FinalizerTest object;
    public void isAlive() {
        System.out.println("I'm alive");
    }
 
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("method finalize is running");
        object = this;
    }
 
    public static void main(String[] args) throws Exception {
        object = new FinalizerTest();
 
        // 第一次執行,finalize方法會自救
        object = null;
        System.gc();
 
        Thread.sleep(500);
        if (object != null) {
            object.isAlive();
        } else {
            System.out.println("I'm dead");
        }
 
        // 第二次執行,finalize方法已經執行過
        object = null;
        System.gc();
 
        Thread.sleep(500);
        if (object != null) {
            object.isAlive();
        } else {
            System.out.println("I'm dead");
        }
    }
}
method finalize is running
I'm alive
I'm dead

從執行結果可以看出: 第一次發生GC時,finalize方法的確執行了,並且在被回收之前成功逃脫; 第二次發生GC時,由於finalize方法只會被JVM呼叫一次,object被回收。

當然了,在實際專案中應該儘量避免使用finalize方法。