垃圾算法與垃圾收集器

摘抄自《深入理解Java虛擬機》

對象是否還活着

可達性分析算法

通過可達性分析來判定對象是否存活。
這個算法的基本思路就是通過一系列的稱爲GC Roots的對象作爲起始點,從起始節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。
在這裏插入圖片描述
object5、object6、object7雖然互相關聯,但是它們到GC Roots是不可達的,所以它們將會被判定爲是可回收的對象。
在java語言中,可作爲GC Roots的對象包括下面幾種:

  • 虛擬機棧中引用的對象(棧幀中的本地常量表)
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 本地方棧中(native方法)引用的對象

引用

判斷對象是否存活都與引用有關
引用分爲強引用(Strong Reference)軟引用(Soft Reference)弱引用(Weak Reference)虛引用(Phantom Reference),這四種引用強度依次逐漸減弱

  • 強引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象
  • 軟引用,描述一些還有用但非必需的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。
  • 弱引用,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。
  • 虛引用,也稱幽靈引用或者幻影引用,一個對象是否有虛引用的存在,完全不會影響其生存時間,無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用的唯一目的就是能在這個對象被收集器回收時收到一個系統通知

生存還是死亡

可達性分析算法中不可達的對象,也並非是非死不可的,這時候它們暫時處於緩刑階段
要真正宣告一個對象死亡,至少要經歷兩次標記過程:
如果對象在進行可達性分析後發現沒有與GC Roots 相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalze()方法,或者finalze()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲沒必要執行
如果這個對象被判定爲有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue的隊列之中,並在稍後由一個由虛擬機自動建立的、低優先級Finalizer線程去執行它。這裏的執行是指虛擬機會觸發這個方法
finalize()方法是對象逃逸死亡命運的最後一次機會,稍後GC將對F-Queue隊列中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關聯即可,那在第二次標記時它將被移除出即將回收的集合;如果對象這時候還沒有逃逸,那基本上他就真的被回收了
finalize()方法只會被系統自動調用一次

回收方法區

java虛擬機規範中說過可以不要求虛擬機在方法區實現垃圾收集,性價比一般比較低
永久代的垃圾收集主要回收兩部分內容:廢棄常量無用類
類需要滿足下面3個條件才能算是無用類

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

垃圾收集算法

標記-清除算法

標記-清除算法是最基礎的收集算法,算法分爲標記清除兩個階段
首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象
它的主要不足有兩個:

  • 效率問題,標記和清除效率都不高
  • 空間問題,標記清除後會產生大量不連續內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作

複製算法

它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。
當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉,這樣內存分配時不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
它的不足是:

  • 把內存縮小了原來的一半,代價太高。

現在的商業虛擬機都採用這種收集算法來回收新生代,IBM公司專門研究表明新生代中的對象98%是朝生夕死,所以並不需要按照1:1的比例來劃分內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用使用Eden和其中一塊Survivor.當回收時,將EdenSurvivor中還存活着的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HoSpot虛擬機默認EdenSurvivor的大小比例是8:1。我們沒辦法保證每次回收後存活的對象在Survivor空間能放下,當Survivor空間不夠用時,需要依賴其他內存(這裏指老年代)進行分配擔保
內存的分配擔保指另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代
不足:

  • 對象存活率較高時就要進行較多的複製操作,效率會變低。
  • 需要額外的空間進行分配擔保

標記-整理算法

標記-整理算法的標記過程仍然與標記-清除算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存

分代收集算法

在這裏插入圖片描述
當前商業虛擬機的垃圾收集都採用分代收集(Generational Collection)算法,這種算法並沒有什麼新的思想,只是根據對象存活週期的不同將內存劃分爲幾塊。一般是把java堆分爲新生代老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。

  • 新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。
  • 老年代中,因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清理或者標記-整理算法來進行回收

HotSpot算法

枚舉根節點

可達性分析對執行時間的敏感體現在GC停頓上,因爲這項分析工作必須在一個能確保一致性的快照中進行,這裏一致性的意思是指整個分析期間整個執行系統看起來就像被凍結在某個時間上,不可以出現分析過程中對象引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證
這點是導致GC執行時必須停頓所有java執行線程(sun將這件事情稱爲stop the word)的其中一個重要原因,即使是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必須要停頓的。
java虛擬機使用的是準確式(即虛擬機可以知道內存中某個位置的數據具體是什麼類型)GC,所以當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文全局的引用位置,虛擬機應當是有辦法直接得知那些地方存放着對象引用。在HotSpot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯(即時編譯)過程中,也會在特定的位置記錄下寄存器中那些位置是引用。這樣GC在掃描時就可以直接得知這些信息了。

安全點

HotSpot沒有爲每條指令都生成OopMap,只是在特定的位置記錄了這些信息,這些位置稱爲安全點(Safepoint),即程序執行時並非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。安全點的選定基本上是以程序是否具有讓程序長時間執行的特徵爲標準進行選定的,長時間執行的最明顯特徵就是指令序列複用,例如方法調用循環跳轉異常跳轉等,所以具有這些功能的指令纔會產生Safepoint
發生GC時讓所有線程(不包括執行JNI調用的線程)都到最近的安全點上再停頓下來有兩種方案可供選擇:

  • 搶先式中斷(Preemptive Suspension),GC時首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓跑到安全點上。現在幾乎沒有虛擬機採用這種方式
  • 主動式中斷(Voluntary Suspension),當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時去主動輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。輪詢標誌的地方和安全點時重合的。

安全區域

Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GCSafepoint,但是程序不執行的時候呢?所謂的程序不執行就是沒有分配CPU時間,典型的例子就是線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求,安全點去中斷掛起。
安全區域Safe Region是指一段代碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被做擴展了的Safepoing
在線程執行到Safe Region中的代碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間裏JVM要發起GC時,就不用管標識自己爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉,如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信號爲止。

垃圾收集器

如果說垃圾收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現
如果兩個收集器之間存在連續,就說明它們可以搭配使用在這裏插入圖片描述

Serial 收集器

Serial(串行)垃圾收集器是最基本、發展歷史最悠久的收集器
JDK1.3.1前是HotSpot新生代收集的唯一選擇

Serial/Serial Old收集器運行示意圖
在這裏插入圖片描述

  • 作用域:新生代
  • 收集算法:新生代Serial採用複製算法,老年代SerialOld採用標記整理算法
  • 線程數:單線程
  • 特點:單線程收集器,在進行收集的時候,需要暫停其他所有工作線程(Stop the world),簡單高效
  • 應用場景:對於桌面應用場景(新生代使用內存不大)是很好的選擇
  • 缺點:在用戶不可見的情況下將用戶的工作線程都停掉,對其他應用造成影響
  • 設置參數

強制指定使用Serial垃圾收集器
“-XX:+UseSerialGC”

ParNew 收集器

ParNew 收集器其實就是Serial收集器的多線程版本

ParNew/Serial Old收集器運行示意圖
在這裏插入圖片描述

  • 作用域:新生代
  • 收集算法:新生代ParNew採用複製算法,老年代SerialOld採用標記整理算法
  • 線程數:多線程
  • 特點:多邏輯CPU時比Serial收集器有更好的效果
  • 應用場景:Server模式下的虛擬機中首選的新生代收集器
  • 缺點:在用戶不可見的情況下將用戶的工作線程都停掉,對其他應用造成影響
  • 設置參數

強制指定使用ParNew垃圾收集器
-XX:+UseParNewGC
CMS收集老年代時,默認ParNew爲新生代收集器
-XX:+UseConcMarkSweepGC
限制垃圾收集的線程數,默認與CPU的數量相同
-XX:ParallelGCThreads

Parallel Scavenge收集器

Parallel Scavenge收集器也經常稱爲吞吐量優先收集器
目標是達到一個可控制的吞吐量(Throughput)
吞吐量:CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

  • 作用域:新生代
  • 收集算法:複製算法
  • 線程數:多線程
  • 特點:高效率地利用CPU時間,儘快完成程序的運算任務
  • 應用場景

高吞吐量爲目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間
當應用程序運行在具有多個CPU上,對暫停時間沒有特別高的要求時,即程序主要在後臺進行計算,而不需要與用戶進行太多交互
那些執行批量處理、訂單處理、工資支付、科學計算的應用程序

  • 缺點:不能與CMS配合使用
  • 設置參數
 -XX:MaxGCPauseMillis 

控制最大垃圾收集停頓時間,大於0的毫秒數,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,收集的新生代變小,垃圾收集更頻繁,停頓時間下降,吞吐量也降下來了

-XX:GCTimeRatio 

設置垃圾收集時間佔總時間的比例,大於0且小於100的整數,公式是1/(1+n) ,默認值爲99,垃圾收集時間佔比爲1%

-XX:+UseAdaptiveSizePolicy

開啓這個參數後,就不用手工指定一些細節參數,如:

  • 新生代的大小-XmnEdenSurvivor區的比例-XX:SurvivorRation、晉升老年代的對象年齡-XX:PretenureSizeThreshold等;

  • JVM會根據當前系統運行情況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics);

  • 這是一種值得推薦的方式:

    1、只需設置好內存數據大小(如-Xmx設置最大堆);

    2、然後使用-XX:MaxGCPauseMillis-XX:GCTimeRatio給JVM設置一個優化目標;

    3、那些具體細節參數的調節就由JVM自適應完成;

這也是Parallel Scavenge收集器與ParNew收集器一個重要區別

Serial Old收集器

Serial收集器的老年版本,同樣是單線程收集器,使用多線程和標記-整理算法
在這裏插入圖片描述

  • 作用域:老年代
  • 收集算法:標記-整理
  • 線程數:單線程
  • 特點:單線程收集器,在進行收集的時候,需要暫停其他所有工作線程(Stop the world),簡單高效
  • 應用場景
    給Client模式下的虛擬機使用
    Server模式有兩大用途:
    1. jdk1.5及以前與Parallel Scavenge收集器搭配使用
    2. 作爲CMS收集器的後備預案,CMS失敗時使用
  • 缺點
  • 設置參數

Parallel Old收集器

Parallel Scavenge收集器的老年版本,使用多線程和標記-整理算法
在這裏插入圖片描述

Parallel

  • 作用域:老年代
  • 收集算法標記整理算法
  • 線程數:多線程
  • 特點:與Parallel Scavenge組合稱爲名副其實的吞吐量優先收集器
  • 應用場景
    1. JDK1.6及之後用來代替老年代的Serial Old收集器
    2. Parallel Scavenge組合使用
  • 缺點
  • 設置參數

指定使用Parallel Old收集器
-XX:+UseParallelOldGC

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。它非常適合在注重用戶體驗的應用上使用
CMS也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)
目的是儘可能地縮短垃圾收集時用戶線程的停頓時間
在這裏插入圖片描述
運作過程:
1. 初始標記(CMS initial mark),需要stop the world,速度很快
2. 併發標記(CMS concurrent mark)
3. 重新標記(CMS remark),需要stop the world,比初始標識時間長一些,遠比並發標記短
4. 併發清除(CMS concurrent sweep)

  • 作用域:老年代
  • 收集算法:標記-清除
  • 線程數:多線程
  • 特點:併發收集、低停頓,需要更多內存
  • 應用場景:與用戶交互較多的場景;希望系統停頓時間最短,注重服務的響應速度
  • 缺點
    1. CMS收集器對CPU資源非常敏感,在併發階段,因爲佔用了一部分線程而導致應用程序變慢,總吞吐量降低
      2.CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現 Concurrent Mode Failure失敗而導致另一次Full GC的產生。這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這部分垃圾稱爲浮動垃圾。垃圾收集時用戶線程在運行,所以需要預留一部分空間提供並收集時的程序運作使用。
      3.CMS基於標記-清除算法,收集結束會有大量空間碎片。無法找到足夠大的連續空間來分配當前對象時就要觸發一次Full GC
  • 設置參數
-XX:+UseConcMarkSweepGC

使用CMS收集器

 -XX:CMSInitiatingOccupancyFraction

GC觸發時老年代使用的百分比,設置太高會導致預留內存無法滿足程序需要,出現Concurrent Mode Failure失敗,這時虛擬機就會臨時啓動Serial Old收集器來重新進行老年代的垃圾收集,停頓時間就很長了。

 -XX:UseCMSCompactAtFullCollection

默認開啓,用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程

 -XX:CMSFullGCsBeforeCompaction

設置執行多少次不壓縮Full GC後,跟着來一次帶壓縮的,默認值爲0,表示每次進入Full GC都進行碎片整理

G1收集器

G1是一款面向服務端應用的垃圾收集器
G1的使命是在未來替換CMS,並且在JDK1.9已經成爲默認的收集器
在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的內存佈局就與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

  • 線程數:多線程

  • 特點

    1. 並行與併發:G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過併發的方式讓java程序繼續執行
    2. 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念;能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配, 採用不同方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果
    3. 空間整理:與CMS的標記清理算法不同,G1從整體來看是基於標記-整理算法實現的收集器,從局部(兩個Region之間)上來看是基於複製算法實現的
    4. 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型。可以明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒。在低停頓的同時實現高吞吐量。
  • 爲什麼G1可以實現可預測停頓

    1. 可以有計劃地避免在Java堆的進行全區域的垃圾收集;

    2. G1收集器將內存分大小相等的獨立區域(Region),新生代和老年代概念保留,但是已經不再物理隔離。

    3. G1跟蹤各個Region獲得其收集價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表;
      每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);

  • 一個對象被不同區域引用的問題

一個Region不可能是孤立的,一個Region中的對象可能被其他任意Region中對象引用,判斷對象存活時,是否需要掃描整個Java堆才能保證準確?

在其他的分代收集器,也存在這樣的問題(而G1更突出):回收新生代也不得不同時掃描老年代?
這樣的話會降低Minor GC的效率;

解決方法:

無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全局掃描:

每個Region都有一個對應的Remembered Set;

虛擬機發現程序在對Reference類型數據寫操作時,都會產生一個Write Barrier暫時中斷操作;

然後檢查將要寫入的引用指向的對象是否和該Reference類型數據在不同的Region(其他收集器:檢查老年代對象是否引用了新生代對象);

如果是,便通過CardTable把相關引用信息記錄到引用指向對象的所在Region對應的Remembered Set中;

當進行垃圾收集時,在GC根節點的枚舉範圍加入Remembered Set;

就可以保證不進行全局掃描,也不會有遺漏。

  • 應用場景

    面向服務端應用,針對具有大內存、多處理器的機器;
    最主要的應用是爲需要低GC延遲,並具有大堆的應用程序提供解決方案;
    如:在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒;
    (實踐:對賬系統中將CMS垃圾收集器修改爲G1,降低對賬時間20秒以上)
    具體什麼情況下應用G1垃圾收集器比CMS好,可以參考以下幾點(但不是絕對):

    1. 超過50%的Java堆被活動數據佔用;
    2. 對象分配頻率或年代的提升頻率變化很大;
    3. GC停頓時間過長(長於0.5至1秒);

建議:

如果現在採用的收集器沒有出現問題,不用急着去選擇G1;
如果應用程序追求低停頓,可以嘗試選擇G1;
是否代替CMS只有需要實際場景測試才知道。(如果使用G1後發現性能還沒有使用CMS好,那麼還是選擇CMS比較好)

在這裏插入圖片描述
不計算維護Remembered Set的操作,可以分爲4個步驟(與CMS較爲相似)。

  1. 初始標記(Initial Marking)

僅標記一下GC Roots能直接關聯到的對象;

且修改TAMS(Next Top at Mark Start),讓下一階段併發運行時,用戶程序能在正確可用的Region中創建新對象;

需要"Stop The World",但速度很快;

  1. 併發標記(Concurrent Marking)

從GC Roots開始進行可達性分析,找出存活對象,耗時長,可與用戶線程併發執行
並不能保證可以標記出所有的存活對象;(在分析過程中會產生新的存活對象)

  1. 最終標記(Final Marking)

修正併發標記階段因用戶線程繼續運行而導致標記發生變化的那部分對象的標記記錄。

上一階段對象的變化記錄在線程的Remembered Set Log;

這裏把Remembered Set Log合併到Remembered Set中;

需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;

G1採用多線程並行執行來提升效率;且採用了比CMS更快的初始快照算法:Snapshot-At-The-Beginning (SATB)。

  1. 篩選回收(Live Data Counting and Evacuation)

首先排序各個Region的回收價值和成本;

然後根據用戶期望的GC停頓時間來制定回收計劃;

最後按計劃回收一些價值高的Region中垃圾對象;

回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另一個空的Region,並且在此過程中壓縮和釋放內存;

可以併發進行,降低停頓時間,並增加吞吐量;

  • 設置參數
指定使用G1收集器:
"-XX:+UseG1GC"

當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45:
"-XX:InitiatingHeapOccupancyPercent"

爲G1設置暫停時間目標,默認值爲200毫秒:
"-XX:MaxGCPauseMillis"

設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region:
"-XX:G1HeapRegionSize"

新生代最小值,默認值5%:
"-XX:G1NewSizePercent"

新生代最大值,默認值60%:
"-XX:G1MaxNewSizePercent"

設置STW期間,並行GC線程數:
"-XX:ParallelGCThreads"

設置併發標記階段,並行執行的線程數:
"-XX:ConcGCThreads"

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章