垃圾收集

整理自《深入理解 Java 虛擬機》。

程序計數器、虛擬機棧、本地方法棧這幾個區域不需要過多考慮回收問題,因爲方法結束或線程結束時,內存自然就隨着回收了。垃圾收集關注的是 Java 堆和方法區這部分內存。

1. 方法區的回收

HotSpot 虛擬機的方法區在永久代中實現,這部分主要回收兩部分內容:廢棄常量和無用的類。

常量池的回收

以常量池中字面量的回收爲例,假如一個學符串 “abc” 已經進入了常量池中,但是當前系統沒有任何一個 String 對象是叫做 “abc” 的,換句話說,就是沒有任何 String 對象引用常量池中的 “abc”’ 常量,也沒有其他地方引用了這個字面量,如果這時發生內存回收,而且必要的話,這個 “abc” 常量就會被系統清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

無用的類的回收

類要滿足 3 個條件才能算是“無用的類”:

  1. 該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  2. 加載該類的 ClassLoader 已經被回收。
  3. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

滿足上述 3 個條件的無用類可以被回收,是否對類進行回收,可以由虛擬機 -Xnoclassgc 參數控制。

2. Java 堆的回收

如何判斷一個對象是否“死亡”?:

引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它時, 計數器值就加 1 ; 當引用失效時,計數器值就減 1;任何時刻計數器爲 0 的對象就是不可能再被使用的。

特點:實現簡單,判定效率高。但主流虛擬機沒有選用引用計數法來管理內存,最主要原因是它很難解決對象之間相互循環引用的問題。

		objA.instance = objB;
		objB.instance = objA;

		objA = null;
		objB = null;
		
		// 假設在這行發生GC,objA 和 objB 不能被回收
		System.gc();

可達性分析算法
該算法的基本思路就是通過一些被稱爲引用鏈(GC Roots)的對象作爲起點,從這些節點開始向下搜索,搜索走過的路徑被稱爲引用鏈(Reference Chain),當一個對象到 GC Roots沒有任何引用鏈相連時(即從GC Roots節點到該節點不可達),則證明該對象是不可用的。
在這裏插入圖片描述
object1~object4 對 GC Root 都是可達的,說明不可被回收,object5 和 object6 雖然相互有關聯,但對 GC Root 節點不可達,說明其可以被回收。

在 Java 中,可作爲 GC Root 的對象包括以下幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象。
  • 方法區中類靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象。

Java 引用概念

  • 強引用(Strong Reference):強引用就是指在程序代碼中普遍存在的,類似 Object obj = new Object() 這類的引用,只要強引用在,垃圾蒐集器永遠不會蒐集被引用的對象。也就是說,寧願出現內存溢出,也不會回收這些對象。
  • 軟引用(Soft Reference):軟引用是用來描述一些有用但並不是必需的對象,在 Java 中用 java.lang.ref.SoftReference 類來表示。對於軟引用關聯着的對象,只有在內存將要發生溢出之前 JVM 纔會回收該對象。
  • 弱引用(Weak Reference):弱引用也是用來描述非必需對象的,當 JVM 進行垃圾回收時,無論內存是否充足,都會回收被弱引用關聯的對象。在java中,用 java.lang.ref.WeakReference 類來表示。
  • 虛引用(Phantom Reference):虛引用和前面的軟引用、弱引用不同,它並不影響對象的生命週期。在 java 中用 java.lang.ref.PhantomReference 類表示。如果一個對象與虛引用關聯,則跟沒有引用與之關聯一樣,在任何時候都可能被垃圾回收器回收。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

判定對象死亡的過程
即使在可達性分析算法中不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。

  1. 第一次標記並進行一次篩選。篩選的條件是此對象是否有必要執行 finalize() 方法。當對象沒有覆蓋 finalize 方法,或者 finzlize 方法已經被虛擬機調用過,虛擬機將這兩種情況都視 爲“沒有必要執行”,對象被回收。
  2. 第二次標記。finalize() 方法是對象脫逃死亡命運的最後一次機會,如果對象在 finalize() 中成功拯救自己——只要重新與引用鏈上的任何的一個對象建立關聯即可,譬如把自己賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出“即將回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。

3. 垃圾收集算法

標記-清除算法

標記清除算法分爲“標記”和“清除”兩個階段,首先先標記出那些對象需要被回收,在標記完成後會對這些被標記了的對象進行回收。
在這裏插入圖片描述
缺點:效率不高,造成內存碎片。

複製算法

複製算法是將內存分爲兩塊大小一樣的區域,每次是使用其中的一塊。當這塊內存塊用完了,就將這塊內存中還存活的對象複製到另一塊內存中,然後清空這塊內存。
在這裏插入圖片描述
現在商用的 JVM 中都採用了這種算法來回收新生代,因爲新生代的對象基本上都是朝生夕死的,存活下來的對象約佔10%左右,所以需要複製的對象比較少,採用這種算法效率比較高。hotspot 版本的虛擬機將堆內存分爲了新生代和老年代,其中新生代又分爲內存較大的 eden 區和兩個較小的 survivor 區。當進行內存回收時,將 eden 區和 survivor 區的還存活的對象一次性地複製到另一個 survivor 空間上,最後將 eden 區和剛纔使用過的 survivor 空間清理掉。hotspot 虛擬機默認 eden 和 survivor 空間的大小比例爲8:1,也就是每次新生代中可用內存空間爲整個新生代空間的90%(80%+10%),只會浪費掉10%的空間。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當survivor 空間不夠用時,需要依賴於其他內存(這裏指的是老年代)進行分配的擔保。

標記-整理算法

複製算法在對象存活率較高的情況下就要進行較多的對象複製操作,效率將會變低。更關鍵的是,如果不想浪費 50% 的空間,就需要有額外的空間進行分配擔保,用以應對被使用的內存中所有對象都 100% 存活的極端情況,所以在老年代一般不能直接選用這種辦法。

根據老年代的特點,有人提出了標記-整理的算法,標記過程仍然與標記-清除算法一樣,但後續步驟不是直接將可回收對象清理掉,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,算法示意圖如下:
在這裏插入圖片描述

分代收集算法

分代收集算法將 heap 區域劃分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。

4. 垃圾收集器

Minor GC 和 Full GC

  • Minor GC 又稱新生代 GC,指發生在新生代的垃圾收集動作,因爲 Java 對象大多是朝生夕滅,所以Minor GC非常頻繁,一般回收速度也比較快。
  • Full GC 又稱 Major GC 或老年代 GC,指發生在老年代的 GC, 出現 Full GC 經常會伴隨至少一次的 Minor GC(不是絕對,Parallel Sacvenge 收集器就可以選擇設置 Major GC策略)。

Major GC 速度一般比 Minor GC 慢10倍以上。

使用分代垃圾收集器,基於以下觀察事實:

  • 大多數分配對象的存活時間短
  • 存活時間久的對象很少引用存活時間短的對象

由此, HotSpot VM 將堆分爲兩個物理區空間,這就是分代。根據新生代和老年代各自的特點,我們應該分別爲它們選擇不同的收集器,以提升垃圾回收效率。
在這裏插入圖片描述
連線的表示可以搭配使用。

名稱 工作區域 單線程/多線程 垃圾收集算法 特點
Serial 新生代 單線程 複製算法 垃圾收集時會暫停其他所有工作線程、簡單高效、適合 Client 模式
Serial Old 老年代 單線程 標記整理算法 Serial 的老年代版本
ParNew 新生代 多線程 複製算法 Serial 的多線程版本、多核情況下減少 GC 時間
Parallel Scavenge 新生代 多線程 複製算法 吞吐量優先、可控制吞吐量、能夠在較短的時間內完成指定任務,適合不需太多交互的後臺運算
Parallel Old 老年代 多線程 標記整理算法 Parallel Scavenge的老年代版本
CMS 老年代 多線程 標記-清除算法 以最短回收停頓時間爲目標、併發收集、缺點:吞吐量低、無法處理浮動垃圾,導致頻繁 Full GC、使用"標記-清除"算法產生碎片空間
G1 新生代、老年代 多線程 整體來看基於標記-整理,局部來看基於複製算法 併發、追求低停頓、可預測的停頓、不會產生內存碎片
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章