Java GC 的那些事(1)
前言
與C語言不同,Java記憶體(堆記憶體)的分配與回收由JVM垃圾收集器自動完成,這個特性深受大家歡迎,能夠幫助程式設計師更好的編寫程式碼,本文以HotSpot虛擬機器為例,說一說Java GC的那些事。
Java堆記憶體
在 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 的那些事(2)JavaGC
- Java GC的那些事(2)JavaGC
- Java 混淆那些事(六):Android 混淆的那些瑣事JavaAndroid
- Java泛型的那些事Java泛型
- JAVA執行緒的那些事?Java執行緒
- 雲原生java的那些事兒Java
- Java字串那些事兒Java字串
- Java中的陣列 - Java那些事兒Java陣列
- 談談 Java 中的那些“瑣”事Java
- Java最困擾你的那些事Java
- Java執行緒池的那些事Java執行緒
- Java反射機制那些事Java反射
- 聊聊java就業那些事Java就業
- Java安全——金鑰那些事Java
- JAVA架構師那些事?Java架構
- Docker的那些事兒—Docker簡介(1)Docker
- 大資料面試那些事(1)大資料面試
- 讓人疑惑的Java程式碼 – Java那些事兒Java
- 說說Java裡的equals(中)- Java那些事兒Java
- 讓人疑惑的Java程式碼 - Java那些事兒Java
- GC那些事兒–Android記憶體優化第一彈GCAndroid記憶體優化
- GC那些事兒--Android記憶體優化第一彈GCAndroid記憶體優化
- Java 資料庫連線的那些事Java資料庫
- 談談java入門的那些事兒Java
- 物件導向 - Java那些事兒物件Java
- Java日誌框架那些事兒Java框架
- Java 混淆那些事(五):ProGuard 其他的選項Java
- java高併發之ConcurrentSkipListMap的那些事Java
- 開源的那些事兒 (1):如何看待開源
- ArrayList初始化 – Java那些事兒Java
- ArrayList初始化 - Java那些事兒Java
- Synchronized的那些事synchronized
- webassembly 的那些事Web
- ViewPager的那些事Viewpager
- Java基礎7:關於Java類和包的那些事Java
- Java long型別和Long型別的那些事Java型別
- Java自動裝箱/拆箱 - Java那些事兒Java
- Java 混淆那些事(一):重新認識 ProGuardJava