與C語言不同,Java記憶體(堆記憶體)的分配與回收由JVM垃圾收集器自動完成,這個特性深受大家歡迎,能夠幫助程式設計師更好的編寫程式碼,本文以HotSpot虛擬機為例,說一說Java GC的那些事。
在JVM記憶體的那些事一文中,我們已經知道Java堆是被所有執行緒共享的一塊記憶體區域,所有物件實例和陣列都在堆上進行記憶體分配。為了進行高效率的垃圾回收,虛擬機器把堆記憶體劃分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3個區域。
新生代由Eden 與Survivor Space(S0,S1)構成,大小透過-Xmn參數指定, Eden 與Survivor Space 的記憶體大小比例預設為8:1,可以透過-XX:SurvivorRatio 參數指定,例如新生代為10M 時,Eden分配8M,S0和S1各分配1M。
Eden:希臘語,意思是伊甸園,在聖經中,伊甸園含有樂園的意思,根據《舊約·創世紀》記載,上帝耶和華照自己的形象造了第一個男人亞當,再用亞當的一個肋骨創造了一個女人夏娃,並安置他們住在了伊甸園。
大多數情況下,物件在Eden中分配,當Eden沒有足夠空間時,會觸發一次Minor GC,虛擬機器提供了-XX:+PrintGCDetails參數,告訴虛擬機器在發生垃圾回收時列印內存回收日誌。
Survivor:意思是倖存者,是新生代和老年代的緩衝區域。
當新生代發生GC(Minor GC)時,會將存活的物件移到S0記憶體區域,並清空Eden區域,當再次發生Minor GC時,將Eden和S0中存活的物件移到S1記憶體區。
存活物件會反覆在S0和S1之間移動,當物件從Eden移動到Survivor或Survivor之間移動時,物件的GC年齡會自動累加,當GC年齡超過預設閾值15時,會將該物件移到老年代,可以透過參數-XX:MaxTenuringThreshold 對GC年齡的閾值進行設定。
老年代的空間大小即-Xmx 與-Xmn 兩個參數之差,用於存放經過幾次Minor GC之後依舊存活的對象。當老年代的空間不足時,會觸發Major GC/Full GC,速度一般比Minor GC慢10倍以上。
在JDK8之前的HotSpot實作中,類別的元資料如方法資料、方法資訊(字節碼,堆疊和變數大小)、執行時常數池、已確定的符號引用和虛方法表等被保存在永久代中,32位元預設永久代的大小為64M,64位元預設為85M,可以透過參數-XX:MaxPermSize進行設置,一旦類別的元資料超過了永久代大小,就會拋出OOM異常。
虛擬機團隊在JDK8的HotSpot中,把永久代從Java堆中移除了,並把類別的元資料直接保存在本地記憶體區域(堆外記憶體),稱之為元空間。
這樣做有什麼好處?
有經驗的同學會發現,對永久代的調優過程非常困難,永久代的大小很難確定,其中涉及到太多因素,如類的總數、常量池大小和方法數量等,而且永久代的資料可能會隨著每一次Full GC而發生移動。
而在JDK8中,類別的元資料保存在本地記憶體中,元空間的最大可分配空間就是系統可用記憶體空間,可以避免永久代的記憶體溢位問題,不過需要監控記憶體的消耗情況,一旦發生記憶體洩漏,會佔用大量的本地記憶體。
ps:JDK7之前的HotSpot,字串常數池的字串被儲存在永久代中,因此可能導致一系列的效能問題和記憶體溢出錯誤。在JDK8中,字串常數池中只保存字串的參考。
GC動作發生之前,需要確定堆記憶體中哪些物件是存活的,一般有兩種方法:引用計數法和可達性分析法。
1、引用計數法
在物件上新增一個引用計數器,每當有一個物件引用它時,計數器加1,當使用完該物件時,計數器減1,計數器值為0的物件表示不可能再被使用。
引用計數法實現簡單,判定高效,但不能解決物件之間相互引用的問題。
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(); } }
透過新增-XX:+PrintGC參數,執行結果:
[GC (System.gc()) [PSYoungGen: 26982K->1194K(75776K)] 26982K->1202K(249344K), 0.0010103 secs]
从GC日志中可以看出objA和objB虽然相互引用,但是它们所占的内存还是被垃圾收集器回收了。
2、可达性分析法
通过一系列称为 “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方法。
Java GC 的那些事(1)
Java GC的那些事(2)
以上就是Java GC 的那些事(1)的内容,更多相关内容请关注PHP中文网(www.php.cn)!