大多數情況下我們對GC的了解都隻是淺層含義上的,下面我們來詳細講解下内部的一些實作原理。
講解GC之前,我們得先了解下JVM的記憶體結構,才能讓我們了解GC導緻是幹嘛的。
一.JVM 記憶體結構
JVM記憶體結構由6個部分組成,分别如下
1. 程式計數器(Program Conuter Register)
一塊較小的記憶體空間,它是目前線程執行位元組碼的行号訓示器,位元組碼解釋工作器就是通過改變這個計數器的值來選取下一條需要執行的指令。它是線程私有的記憶體,也是唯一一個沒有OOM異常的區域。
2. Java虛拟機棧區(Java Virtual Machine Stacks)
也就是通常所說的棧區,它描述的是Java方法執行的記憶體模型,每個方法被執行的時候都建立一個棧幀(Stack Frame),用于存儲局部變量表、操作數棧、動态連結、方法出口等。每個方法被調用到完成,相當于一個棧幀在虛拟機棧中從入棧到出棧的過程。此區域也是線程私有的記憶體,可能抛出兩種異常:如果線程請求的棧深度大于虛拟機允許的深度将抛出StackOverflowError;如果虛拟機棧可以動态的擴充,擴充到無法動态的申請到足夠的記憶體時會抛出OOM異常。
3. 本地方法棧(Native Method Stacks)
本地方法棧與虛拟機棧發揮的作用非常相似,差別就是虛拟機棧為虛拟機執行Java方法,本地方法棧則是為虛拟機使用到的Native方法服務。
4. 堆區(Heap)
所有對象執行個體和數組都在堆區上配置設定,堆區是GC主要管理的區域。堆區還可以細分為新生代、老年代,新生代還分為一個Eden區和兩個Survivor區。此塊記憶體為所有線程共享區域,當堆中沒有足夠記憶體完成執行個體配置設定時會抛出OOM異常。
5. 方法區(Method Area)
方法區也是所有線程共享區,用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯後的代碼等資料。GC在這個區域很少出現,這個區域記憶體回收的目标主要是對常量池的回收和類型的解除安裝,回收的記憶體比較少,是以也有稱這個區域為永久代(Permanent Generation)的。當方法區無法滿足記憶體配置設定時抛出OOM異常。
二.GC 原理
針對GC的原理機制,主要搞清楚下面三個問題。
什麼時候回收?
哪些需要回收?
怎麼回收?
1. 什麼時候回收?
1.對象優先配置設定到新生代的Eden區,當不夠空間的時候進行一次Minor GC,清理頻率很高。
2.Full GC發生在老年代,當不夠空間的時候進行一次Full GC,伴随着也會進行一次Minor GC。
3.進行Minor GC時,會判斷每次變成晉升到老年代的對象平均值是否大于老年代剩餘空間,如果大于,則進行一次Full GC,如果小于就會去判斷HandlePromotionFailure設定是否允許擔保失敗,如果允許,則進行Minor GC,不允許則改為Full GC。
上面提到GC主要管理的是堆區,堆區主要分為`新生代`和`老年代`
* 【新生代】分為一個Eden和兩個Survivor區。新new的對象都放在這裡,很快消亡。
* 【老年代】新new的大對象直接丢到這裡(為了避免在Eden區和兩個Survivor區發生大量的記憶體拷貝),其餘就是在新生代多次回收沒被幹掉過來變成老家夥的對象了。
2. 哪些需要回收?
當調用了finalize()方法後,則需要進行清理,具體場景如上。
* 什麼是finalize()方法?
每次進行GC之前系統都會調用一次finalize()方法,用以清理所有活動并且釋放資源。
* 什麼時候調用finalize()方法?
1.GC調用之前,例如運作System.gc();(調用System.gc()隻是建議JVM去執行,是否執行還得JVM去判斷)
2.程式退出時,每個對象都會調用finalzie
3.顯式調用finalize
3. 怎麼回收?
JVM會根據不同的收集器使用不同的算法組合來達到回收的效果
### 垃圾收集算法
* mark-sweep(标記-清除)- 标記所有需要回收的對象,在标記完成後統一回收這些對象。
缺點:1.标記和清除兩個過程的效率都不高。2.标記清除會産生大量不連續的記憶體碎片。
* copying(複制)- 主要用來回收新生代
新生代分為一個Eden區、兩個Survivor區(Survivor0、Survivor1),Eden和Survivor預設8:1。回收時先把Eden存活對象複制到Survivor0區,清空Eden區,當Survivor0區滿了以後,把Eden和Survivor0區的存活對象複制到Survivor1區,清空Eden區和Survivor0區,之後交換Survivor0和Survivor1區,保持Survivor1區是空的,如此往複
* mark-compact(标記-整理)
主要用來回收老年代。标記需要回收的對象,将其他存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。
* generational collection(分代)
目前常用的收集算法,區分新生代和老年代做不同的算法收集。針對新生代,隻有少量存活,選用複制算法。針對老年代,存活率高,沒有額外的空間對它進行配置設定擔保,就必須使用“标記-清理”或者“标記-整理”算法來進行回收。
--------
### 垃圾收集器
* Serial 、Serial Old 收集器 [-XX:+UseSerialGC,使用 Serial + Serial Old 組合回收]
适合單處理器系統,并且在進行垃圾回收的時候會暫停其他所有的工作線程(stop the world),對于多處理器的系統來說是災難
* ParNew 收集器 [-XX:UseParallelGC,使用 Parallel Scavenge + Serial Old 組合回收]
serial 收集器的多線程版本
* Parallel Scavenge 、Parallel Old 收集器 [-XX:GCTimeRatio,-XX:MaxGCPauseMillis]
通過兩個參數GCTimeRatio和MaxGCPauseMillis,盡可能縮短垃圾收集器使用者線程的停頓時間,進而達到一個可控制的吞吐量。
* CMS (Concurrent Mark Sweep)收集器 [-XX:UseConcMarkSweepGC,使用 ParNew + CMS + Serial Old 組合回收]
以擷取最短回收停頓時間為目标的收集器。
步驟
1.初始标記(CMS initial mark),标記GC Roots能直接關聯到的對象,速度很快
2.并發标記(CMS concurrent mark),進行GC Roots Tracing
3.重新标記(CMS remark),重新标記階段是為了修正并發标記期間因使用者程式繼續運作而導緻變動的标記記錄,比并發标記時間短
4.并發清除(CMS concurrent sweep)并行删除
缺點
1. 比較消耗CPU資源,預設啟動回收線程數是(CPU數量+3)/4。
2. CMS收集器無法處理浮動垃圾(CMS清理階段使用者線程還運作着,伴随生成的新垃圾隻能在下次GC再清理掉),可能出現“Concurrent Mode Failure”而導緻另一次Full GC的産生。可以通過-XX:CMSInitiatingOccupancyFraction來調節。
3. 标記-清除會導緻記憶體碎片而導緻觸發Full GC(切換到Serial Old收集器收集老年代)。可以通過-XX:UseCMSCompactAtFullCollection、-XX:CMSFullGCsBeforeCompaction來調節。
* G1(Garbage-First) 收集器 [-XX:+UseG1GC]
Java堆的記憶體分布和其他收集器有很大不同,它将整個Java堆劃分為多個大小相等的獨立區域`Region`,老年代和新生代不再實體隔離,而是一部分`Region`的集合。G1會跟蹤各個Region的垃圾堆積價值大小,背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的`Region`。
特點
1.并發和并行
2.分代收集
3.空間整合
4.可預測的停頓
步驟
1.初始标記(Initial Marking)
2.并發标記(Concurrent Marking)
3.最終标記(Final Marking)
4.篩選回收(Live Data Counting and Evacuation)