javaGC回收機制
在面試java后端開發的時候一般都會問到java的自動回收機制(GC)。在了解java的GC回收機制之前,我們得先了解下Java虛擬機的內存區域。
java虛擬機運行時數據區
java虛擬機在執行的過程中會將其管理的內存划分為不用的數據區域,不同的區域有不同的作用以及線程時間。
數據區划分如下:

下面將介紹不同區域的作用,如果已經了解可以跳過
-
程序計數器(線程私有)
程序計數器的作用很簡單,就是記錄當前線程所執行的位置(所以為線程私有),可以看成當前線程所執行的字節碼的行號指示器。如果執行的是native方法,則這個計數器為空。
-
Java虛擬機棧(線程私有,生命周期與線程相同)
虛擬機棧描述的是Java方法執行的內存模型:每個Java方法在執行的時候都會創建一個棧幀(Stack Frame)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
-
本地方法棧(線程共享)
本地方法棧與虛擬機棧發揮的作用類似,不過它執行的是虛擬機使用的Native方法。
-
Java堆(線程共享)
Java堆是Java虛擬機管理內存中最大的一塊,在虛擬機啟動的時候創建。此區域的唯一目的就是存放對象示例,幾乎所有的對象實例都是在這分配內存的。
-
方法區(線程共享)
剛開始的時候,看到方法區域,第一想法就是
Java中的方法
,不過實際上並不是這樣。方法區儲存的是已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯后的代碼等數據。我們可以想一想,當我們需要創建一個對象的時候,我們需要根據類的信息去創建,那么類的信息在哪?當然是在方法區!-
運行時常量池
運行時常量池是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。
-
垃圾收集(Garbage Collection)GC
前面說了這么多,現在我們終於可以來說說垃圾回收機制了。
首先我們得說下垃圾回收回收的是哪一部分內存區域。在前面我們知道:程序計數器,虛擬機棧,本地方法棧都是線程私有的,隨着線程生或滅。這部分我們就不需要考慮了。所以我們需要考慮的就是Java堆
和方法區
。
垃圾回收的內容
回收java堆
-
對象是否可以被回收
判斷對象是否被回收就是當一個對象死了的時候就需要進行回收。那么如何判斷一個對象是否死亡,在Java中,我們使用了可達性分析算法來判斷對象是否存活。
當一個對象到GC Roots沒有任何鏈(稱為
引用鏈
)相連(也就是對象到GC Roots不可達)則判定對象已經死亡(如圖中的Object5,Object6),可進行回收。可作為GC Roots的對象:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中類靜態屬性引用的對象
- 方法區中常量引用的對象
- 本地方法棧中JNI(即一般說的Native方法)引用的對象
在前面中,我們知道,不可達就意味着回收,可是當我們的內存很夠時,有一些對象又是“食之無味棄之可惜”的時候,我們怎么辦呢?在JDK1.2中,Java對引用進行擴張,分為以下引用:
- 強引用(Strong Reference):只要強引用還在,則不回收
- 軟引用(Soft Reference):描述一些有用但非必須的對象,在系統將要發生內存溢出之前,將這些對象列入回收范圍之中進行第二次回收。<java.lang.ref.SoftReference>
- 弱引用(Weak Reference):比軟引用還要弱,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。<java.lang.ref.WeakReference>
- 虛引用(Phantom Reference):不會對生存時間構成影響,唯一的作用就是這個對象被回收的時候會收到一個通知。<java.lang.ref.PhantomReference>
-
最終判斷對象是否能夠存活
在可達性分析算法中,如果一個對象不可達,那么這個對象就進入到了“緩刑”階段,真正宣告一個對象死亡還需要進行兩次標記。
-
第一次標記進行篩選
對不可達的對象進行第一次標記並進行篩選。篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過(意思就是finalize()方法只能被調用一次,也就是對象只能夠有一次避免被回收),虛擬機將這兩種情況都視為“沒有必要執行”,對象被回收。
-
第二次標記
如果這個對象被判定為有必要執行finalize()方法,那么這個對象將會被放置在一個名為:F-Queue的隊列之中,並在稍后由一條虛擬機自動建立的、低優先級的Finalizer線程去執行。這里所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),將很可能會導致F-Queue隊列中的其他對象永久處於等待狀態,甚至導致整個內存回收系統崩潰。
finalize()方法是對象脫逃死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模標記,如果對象要在finalize()中成功拯救自己----只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。
-
回收方法區
在Java虛擬機規范中說過不要求方法區實現垃圾收集,並且進行垃圾收集的“性價比”也較低。不過既然寫了,那必定有方法區的垃圾收集,主要回收以下兩部分內容:
-
廢棄常量:字面量和符號引用
-
無用的類:
- 該類的所有實例都被回收,即:Java堆中不存在該類的任何實例
- 該類的Classloader已經被回收
- 該類對用的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射訪問到該類的方法。
當滿足以上三個條件時,也未必說是一定要被回收。也僅僅是可以。
垃圾收集算法
年代划分
我們通過對象的存活周期來將JVM堆中內存空間划分為新生代和老年代。
-
新生代:主要是用來存放新生的對象。一般占據堆的1/3空間。
-
老年代:主要存放應用程序中生命周期長的內存對象。
算法
OK,說了這么多,我們現在終於可以來說說垃圾收集的算法了。
下面的圖片來源於這位大佬,這位大佬講的真滴不錯。
-
標記-清除算法(Mark-Sweep)
標記:首先標記需要回收的對象,標記完成統一回收
清除:就是清除對象,釋放空間
缺點:標記和清除的效率不高,同時產生大量不連續的內存碎片(可能不利於下次的空間分配)。
-
標記整理法
標記整理算法相比較於標記清除算法,標記-整理算法在清除的時候並不是一個一個的清除對象釋放空間,而是一次清除全部的可回收的空間。這樣使得空間變得連續,有利於對象空間的分配。
-
復制算法
- 將內存分成兩塊大小相等的空間。
- 每次使用其中一塊。
- 進行垃圾回收的時候,將不要的回收的對象復制到另外一個空間
- 完全清除原來的空間。
優點:速度快,效率高,不會產生內存碎片。
缺點:顯而易見,空間浪費大,縮小了一半。
解決方法:
IBM研究表明:新生代98%的對象是“朝生夕死”,所以我們並不需要將空間划分為1:1,而是將空間划分為
Eden:Survivor:Survivor = 8:1:1
。每次使用Eden和其中一塊Survivor。- 使用其中Eden和一塊Survivor。
- 進行回收時,講Eden和Survivor還存活的對象一次性的復制到另外一塊Survivor上。
- 清理第一步中的Eden和Survivor。
如果第二步中Survivor的空間不足,則依賴於其他內存(老年代)進行分配擔保(也就是講存活的對象放入老年代)。
-
分代收集算法
分代收集算法其實就是前面幾種算法的應用。根據年代使用不同的算法
- 新生代GC(MinorGC,回收速度快):復制算法
- 老年代(Full GC/Major GC,比Minor慢10倍以上):標記整理法和標記清除法。
對象分配內存區域
- 新生代:大多數情況下愛,對象在新生代Eden區中分配。如果沒有足夠的空間,則發起一次MinorGC。
- 老年代:
- 大對象直接進入老年代。比如說很長的字符串或數組。
- 長期存活的對象:沒熬過一次MinorGC,年齡age增加一歲,當它的年齡超過一定歲數時(默認15,可設置),則進入老年代中。
- 動態對象年齡判定:如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代。
參考書籍:《深入理解Java虛擬機》——周志明,這本書寫的太好了,寫的通熟易懂。強烈推薦去看看。