大多數情況下我們對GC的了解都只是淺層含義上的,下面我們來詳細講解下內部的一些實現原理。
講解GC之前,我們得先了解下JVM的內存結構,才能讓我們理解GC導致是干嘛的。
JVM內存結構由6個部分組成,分別如下
一塊較小的內存空間,它是當前線程執行字節碼的行號指示器,字節碼解釋工作器就是通過改變這個計數器的值來選取下一條需要執行的指令。它是線程私有的內存,也是唯一一個沒有OOM異常的區域。
也就是通常所說的棧區,它描述的是Java方法執行的內存模型,每個方法被執行的時候都創建一個棧幀(Stack Frame),用于存儲局部變量表、操作數棧、動態鏈接、方法出口等。每個方法被調用到完成,相當于一個棧幀在虛擬機棧中從入棧到出棧的過程。此區域也是線程私有的內存,可能拋出兩種異常:如果線程請求的棧深度大于虛擬機允許的深度將拋出StackOverflowError;如果虛擬機棧可以動態的擴展,擴展到無法動態的申請到足夠的內存時會拋出OOM異常。
本地方法棧與虛擬機棧發揮的作用非常相似,區別就是虛擬機棧為虛擬機執行Java方法,本地方法棧則是為虛擬機使用到的Native方法服務。
所有對象實例和數組都在堆區上分配,堆區是GC主要管理的區域。堆區還可以細分為新生代、老年代,新生代還分為一個Eden區和兩個Survivor區。此塊內存為所有線程共享區域,當堆中沒有足夠內存完成實例分配時會拋出OOM異常。
方法區也是所有線程共享區,用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據。GC在這個區域很少出現,這個區域內存回收的目標主要是對常量池的回收和類型的卸載,回收的內存比較少,所以也有稱這個區域為永久代(Permanent Generation)的。當方法區無法滿足內存分配時拋出OOM異常。
針對GC的原理機制,主要搞清楚下面三個問題。
什么時候回收?哪些需要回收? 怎么回收?上面提到GC主要管理的是堆區,堆區主要分為新生代和老年代
【新生代】分為一個Eden和兩個Survivor區。新new的對象都放在這里,很快消亡。 【老年代】新new的大對象直接丟到這里(為了避免在Eden區和兩個Survivor區發生大量的內存拷貝),其余就是在新生代多次回收沒被干掉過來變成老家伙的對象了。
對象優先分配到新生代的Eden區,當不夠空間的時候進行一次Minor GC,清理頻率很高。Full GC發生在老年代,當不夠空間的時候進行一次Full GC,伴隨著也會進行一次Minor GC。進行Minor GC時,會判斷每次變成晉升到老年代的對象平均值是否大于老年代剩余空間,如果大于,則進行一次Full GC,如果小于就會去判斷HandlePromotionFailure設置是否允許擔保失敗,如果允許,則進行Minor GC,不允許則改為Full GC。1.什么是finalize()方法?
每次進行GC之前系統都會調用一次finalize()方法,用以清理所有活動并且釋放資源。
2.什么時候調用finalize()方法?
GC調用之前,例如運行System.gc();(調用System.gc()只是建議JVM去執行,是否執行還得JVM去判斷) 程序退出時,每個對象都會調用finalzie 顯式調用finalize3.什么是對象不可達?
當一個對象沒有任何引用連接的時候,則說明對象不可達,即對象不可用,這個時候就需要進行GC清理。
判斷對象是否可達的依據是有沒必要執行finalize()方法。如果finalize()方法沒有被覆蓋或者已經被系統調用過一次了(每個對象生命周期內只能調用一次),則被不可達,需要進行GC清理,否則進行自救,恢復引用連接。
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 組合回收)
以獲取最短回收停頓時間為目標的收集器。
步驟
初始標記(CMS initial mark),標記GC Roots能直接關聯到的對象,速度很快
并發標記(CMS concurrent mark),進行GC Roots Tracing
重新標記(CMS remark),重新標記階段是為了修正并發標記期間因用戶程序繼續運作而導致變動的標記記錄,比并發標記時間短
并發清除(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。
特點
并發和并行分代收集空間整合可預測的停頓步驟
初始標記(Initial Marking)并發標記(Concurrent Marking)最終標記(Final Marking)篩選回收(Live Data Counting and Evacuation)新聞熱點
疑難解答