JVM的垃圾回收機制原理

日常開發中,我們總需要創建大量的對象,如果我們沒有及時把創建的對象回收,造成對象持續堆積,直至造成內存溢出問題。JVM提供了一種垃圾回收機制,在後臺創建一個守護線程,在內存緊張的時候自動進行垃圾回收機制。

一、Java的內存結構模型

Java虛擬機在執行Java程序的時候會把它所管理的內存劃分爲若干個不同的數據區域,而有些區域是每個線程的私有區域,有些區域是所有線程所共享的共有區域。
在這裏插入圖片描述
從圖中我們可以看出,虛擬機棧、本地方法棧、程序計數器屬於線程私有區域。隨着線程的創建和關閉進行創建和取消。其中,

  • 程序計數器可以看作是當前線程所執行的字節碼的行號指示器,工作時就是通過改變計數器的值來選取下一條需要執行的字節碼指令。而程序計數器只記錄正在執行的字節碼指令地址,如果執行本地Native方法,則計數器的值爲空。
  • Java虛擬機棧描述的是Java方法執行的線程內存模型,每個方法被創建的時候,虛擬機都會同步創建一個棧幀用來存儲局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法執行被調用到執行完畢的過程,就對應着一個棧幀從虛擬機棧入棧到出棧的過程。
  • 本地方法棧和虛擬機棧非常相似,爲本地的Native方法提供方法服務。
  • 堆存放Java中所有的對象實例,所有的對象在創建時候都會在這裏分配內存。
  • 方法區和Java堆一樣,也是各個線程共享的內存區域,用於存儲被虛擬機機加載的類型信息、常量、靜態變量、編譯後的代碼緩存等。

二、如何判斷Java對象是否存活

爲了判斷一個對象是否應該被回收,JVM給我們提供了兩種算法:引用計數算法可達性分析法

2.1、引用計數算法

爲每一個對象添加一個引用計數器,用來存儲該對象被引用的個數。當該個數爲0時,意味着沒人引用這個對象,可以認爲這個對象死亡。每當有一個地方去引用它時,引用計數器就+1。
但是,這種方法存在一個問題:當兩個對象相互引用時,它倆的計數就永遠不爲0,就永遠不會被回收。例如:當兩個類互相是對方的成員變量,重寫toString方法的時候,相互調用,就會造成循環引用。

2.2 可達性分析法

這種方法的思路是把所有對象之間的關係想象成一顆樹,從樹的根節點GC Roots出發,持續遍歷出所有連接的樹枝對象,這些對象被看作是存活對象。
可以作爲GC Root節點的對象,主要有如下四種:

  • 虛擬機棧中引用的對象
  • 方法區中靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方法棧中JNI引用的對象。

可達性分析法併發時可能產生的“對象”消失問題: 使用增量更新和原始快照兩個方法解決。

增量更新: 在遍歷過程中,如果添加了一條由已訪問過節點指向未訪問過節點的引用,我們就需要把這條引用記錄下來,待併發掃描過後,再按照這些記錄下來的引用關係的已訪問節點重新掃描一次。

原始快照:在遍歷過程中,如果刪除了一條指向未訪問節點的引用關係時,就要把這個要刪除的引用記錄下來,然後待併發掃描完成後,再將這些記錄過的引用關係重新掃描一遍。

三、垃圾回收算法

通過上面的算法,我們可以明確標記出垃圾對象。接下來,JVM還同樣爲我們提供了一些算法,去回收這些垃圾對象。

3.1 標記 - 清除算法

這個算法分爲兩個階段:首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記所有存活的對象,再統一回收掉所有未被標記的對象。標記的過程就是對象是否屬於垃圾的判定過程。

優點:簡單方便
缺點:容易產生大量的磁盤碎片,執行效率不穩定。

3.2 標記 - 複製算法

爲了避免大量回收對象效率低的問題,這種算法把可用內存按容量劃分成大小相等的兩塊,每次只使用其中的一塊。這一塊內存用完了,就將還存活者的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

優點:這種算法適用於回收新生代,並且分配內存時不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。
缺點:顯而易見,這種算法的代價是內存縮小爲原來的一半,空間浪費過大。

3.3 標記 - 整理算法

針對老年代的數據特徵,標記整理算法中標記過程與標記-清除算法一樣,但後續步驟不是直接將可回收對象進行清理,而是讓所有存活的對象都向內存空間的另一端移動,然後直接清除掉邊界以外的內存。

特點:特別適合老年代存活對象中,垃圾少的情況,並且每次整理後都有大塊的空間來存儲大對象。
缺點:整理過程過於複雜,算法複雜程度較高。

3.4 堆和方法區的內存回收

3.4.1 方法區的垃圾回收

方法區又稱作永久代,垃圾回收主要包括兩部分:廢棄常量和無用的類。
首先是廢棄常量垃圾回收的一般步驟:
第一步:判斷一個常量是否是廢棄常量:沒有任何一個地方對這個常量進行引用。
第二步:垃圾回收

無用的類垃圾回收的一般步驟:
第一步:判斷一個類是否是無用的類,看是否滿足下面三個條件:

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

3.4.2 堆的垃圾回收—分代回收算法

Java堆分成三個部分,分別用來存儲三種類型的數據:

  • 剛創建的對象 — 新生代

  • 存活了一段時間的對象 — 新生代

  • 永久存在的對象 — 老年代
    針對這幾種對象,有如下方案:

  • 新生代- 標記-複製回收算法:對於新生區域,每次GC都有大量新對象死去,少量存活。因此採用複製回收算法,把少量存活的對象複製過去即可。

  • 老年代 - 標記整理 回收算法:老年代對象存活多,垃圾少,並且對象體積較大。根據這個特點,只少量的移動對象就能清除垃圾。而且不存在磁盤碎片會。所以應用標記整理算法。只需根據標記整理的具體步驟進行垃圾回收即可。

四、常見垃圾回收器

在JVM中,常見的垃圾回收器有Serial、ParNew、Parallel Scavenge、CMA、Serial Old(MSC)、Parallel Old 、 G1等。下面對這些垃圾回收器進行一個簡要介紹

  • Serial(單線程): 這種回收器時最基本的新生代垃圾回收器,是單線程的垃圾回收期。採用的是 複製算法。垃圾清理時,Serial回收器不存在單線程的切換,所以單CPU環境下,垃圾清除效率較高。
  • Serial Old(單線程) Serial Old 是Serial 回收器的老年代版本,也是單線程回收器。使用標記-整理算法。
  • ParNew(多線程) 是在Serial回收器的基礎上演化來的,屬於Serial回收器的多線程版本,採用複製算法。運行在新生代區域。可以根據CPU核數來開啓不同的線程數,從而達到最優的垃圾回收效果。
  • Parallel Scavenge(多線程) 也是運行在新生代區域,屬於多線程的回收器,採用標記—複製算法。而與ParNew不同的是,Parallel Scavenge回收器更關心的是程序運行的吞吐量。即一段時間內用戶代碼運行時間佔總時間的百分比。
  • Parallel Old(多線程) 是Parallel Scavenge回收器的老年代版本,屬於多線程回收器,採用標記—整理算法,同樣考慮吞吐量優先這一指標。
  • CMS(多線程) 是在最短回收停頓時間爲前提的回收器,屬於多線程回收器,採用標記—清除算法。適用於B/S結構的服務器。
  • G1回收器 G1是JDK1.7之中用來取代CMS的壓縮回收期,物理上沒有隔斷新生代和老年代,但扔然屬於分代垃圾回收器。
    在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章