天天看點

JVM垃圾回收算法

JVM GC回收哪個區域内的垃圾?

需要注意的是,JVM GC隻回收堆區和方法區内的對象。而棧區的資料,在超出作用域後會被JVM自動釋放掉,是以其不在JVM GC的管理範圍内。

Java方法區在Sun HotSpot虛拟機中被稱為永久代,很多人認為該部分的記憶體是不用回收的,java虛拟機規範也沒有對該部分記憶體的垃圾收集做規定,但是方法區中的廢棄常量和無用的類還是需要回收以保證永久代不會發生記憶體溢出。

判斷廢棄常量的方法:如果常量池中的某個常量沒有被任何引用所引用,則該常量是廢棄常量。

判斷無用的類:

(1).該類的所有執行個體都已經被回收,即java堆中不存在該類的執行個體對象。

(2).加載該類的類加載器已經被回收。

(3).該類所對應的java.lang.Class對象沒有任何地方被引用,無法在任何地方通過反射機制通路該類的方法。

JVM GC怎麼判斷對象可以被回收了?

1)對象沒有引用

2)作用域發生未捕獲異常

3)程式在作用域正常執行完畢

4)程式執行了System.exit()

5) 程式發生意外終止(被殺線程等)

JVM GC什麼時候執行?

eden區空間不夠存放新對象的時候,執行Minro GC。升到老年代的對象大于老年代剩餘空間的時候執行Full GC,或者小于的時候被HandlePromotionFailure 參數強制Full GC 。

調優主要是減少 Full GC 的觸發次數,可以通過 NewRatio 控制新生代轉老年代的比例,通過MaxTenuringThreshold 設定對象進入老年代的年齡閥值(後面會介紹到)。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k

-Xmx3550m:設定JVM最大可用記憶體為3550m。

-Xms3550m:設定JVM初始記憶體為3550m。

此值可以設定與 -Xmx 相同,以避免每次垃圾回收完成後JVM重新配置設定記憶體。

-Xmn2g:設定年輕代大小為2G。整個堆大小=年輕代大小+老年代大小+永久代大小。永久代一般固定大小為64m,是以增大年輕代後,将會減小老年代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。

-Xss128k:設定每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小為1M,以前每個線程堆棧大小為256k。根據應用的線程所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的線程。但是作業系統對一個程序内的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與老年代的比值(除去永久代)。設定為4,則年輕代與老年代所占比值為1:4,年輕代占整個堆棧的1/5。

-XX:SurvivorRatio=4:設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6。

-XX:MaxPermSize=16m:設定永久代大小為16m。

-XX:MaxTenuringThreshold=0:設定垃圾最大年齡。如果設定為0的話,則年輕代對象不經過Survivor區,直接進入老年代。對于老年代比較多的應用,可以提高效率。如果此值設定為一個較大值,則年輕代對象會在Survivor區進行多次複制,這樣可以增加對象在年輕代的存活時間,增加在年輕代被回收的機率。

MaxTenuringThreshold這個參數用于控制對象能經曆多少次Minor GC才晉升到老年代,預設值是15

按代的垃圾回收機制

新生代(Young generation):絕大多數最新被建立的對象都會被配置設定到這裡,由于大部分在建立後很快變得不可達,很多對象被建立在新生代,然後“消失”。對象從這個區域“消失”的過程我們稱之為:Minor GC 。

老年代(Old generation):對象沒有變得不可達,并且從新生代周期中存活了下來,會被拷貝到這裡。其區域配置設定的空間要比新生代多。也正由于其相對大的空間,發生在老年代的GC次數要比新生代少得多。對象從老年代中消失的過程,稱之為:Major GC 或者 Full GC。

持久代(Permanent generation)也稱之為 方法區(Method area):用于儲存類常量以及字元串常量。注意,這個區域不是用于存儲那些從老年代存活下來的對象,這個區域也可能發生GC。發生在這個區域的GC事件也被算為 Major GC 。隻不過在這個區域發生GC的條件非常嚴苛,必須符合以下三種條件才會被回收:

1、所有執行個體被回收

2、加載該類的ClassLoader 被回收

3、Class 對象無法通過任何途徑通路(包括反射)

如果老年代的對象需要引用新生代的對象,會發生什麼呢?

為了解決這個問題,老年代中存在一個 card table ,它是一個512byte大小的塊。所有老年代的對象指向新生代對象的引用都會被記錄在這個表中。當針對新生代執行GC的時候,隻需要查詢 card table 來決定是否可以被回收,而不用查詢整個老年代。這個 card table 由一個write barrier 來管理。write barrier給GC帶來了很大的性能提升,雖然由此可能帶來一些開銷,但完全是值得的。

預設的新生代(Young generation)、老年代(Old generation)所占空間比例為 1 : 2

新生代空間的構成與邏輯

1) 一個伊甸園空間(Eden)

2) 兩個幸存者空間(Fron Survivor、To Survivor)

預設新生代空間的配置設定:Eden : From : To = 8 : 1 : 1

剛建立的對象會被配置設定在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的記憶體空間,是以在其上配置設定記憶體極快;最初一次,當Eden區滿的時候,執行Minor GC,将消亡(不可達)的對象清理掉,并将剩餘(存活)的對象複制到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);下次Eden區滿了,再執行一次Minor GC,将消亡的對象清理掉,将存活的對象複制到Survivor1中,然後清空Eden區;将Survivor0中消亡的對象清理掉,将其中可以晉級的對象晉級到Old區,将存活的對象也複制到Survivor1區,然後清空Survivor0區;當兩個存活區切換了幾次(HotSpot虛拟機預設15次,用-XX:MaxTenuringThreshold控制,大于該值進入老年代,但這隻是個最大值,并不代表一定是這個值)之後,仍然存活的對象(其實隻有一小部分,比如,我們自己定義的對象),将被複制到老年代。
JVM垃圾回收算法
  • 新生代: 
    • 大多數新生的對象在Eden區配置設定,當Eden區沒有足夠空間進行配置設定時,虛拟機就會進行一次MinorGC。
    • 在方法中new一個對象,方法調用完畢,對象就無用,這就是典型的新生代對象。(新生對象在Eden區經曆過一次MinorGC并且被Survivor容納的話,對象年齡為1,并且每熬過一次MinorGC,年齡就會加1,直到15,就會晉升到老年代)
    • 注意動态對象的判定:Survivor空間中相同年齡的對象大小總和大于Survivor空間的一半,大于或者等于該年齡的對象就可以直接進入老年代。
  • 老年代: 
    • 在新生代中經曆了N次垃圾回收後仍然存活的對象,就會被放到老年代中,而且大對象(占用大量連續記憶體空間的java對象如很長的字元串及數組)直接進入老年代。
    • 當survivor空間不夠用時,需要依賴老年代進行配置設定擔保。
  • 永久代: 
    • 方法區
    • 主要存放Class和Meta的資訊,Class在被加載的時候被放入永久代。 它和存放對象的堆區域不同,GC(Garbage Collection)不會在主程式運作期對永久代進行清理,是以如果你的應用程式會加載很多Class的話,就很可能出現PermGen space錯誤。

GC分類

    1. MinorGC:是指清理新生代
    2. MajorGC:是指清理老年代(很多MajorGC是由MinorGC觸發的)
    3. FullGC:是指清理整個堆空間包括年輕代和永久代

JVM GC 算法講解

1、根搜尋算法

根搜尋算法是從離散數學中的圖論引入的,程式把所有引用關系看作一張圖,從一個節點GC ROOT 開始,尋找對應的引用節點,找到這個節點後,繼續尋找這個節點的引用節點。當所有的引用節點尋找完畢後,剩餘的節點則被認為是沒有被引用到的節點,即無用的節點。

目前Java中可以作為GC ROOT的對象有:

1、Java虛拟機棧中引用的對象(棧幀中的本地變量表)

2、方法區中靜态屬性引用的對象

3、方法區中常量引用的對象

4、本地方法棧中JNI本地方法引用的對象(Native對象)

2、标記 - 清除算法

标記-清除算法采用從根集合進行掃描,對存活的對象進行标記,标記完畢後,再掃描整個空間中未被标記的對象進行直接回收。

标記-清除算法不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活的對象比較多的情況下極為高效,但由于标記-清除算法直接回收不存活的對象,并沒有對還存活的對象進行整理,是以會導緻記憶體碎片。

3、複制算法

複制算法将記憶體劃分為兩個區間,使用此算法時,所有動态配置設定的對象都隻能配置設定在其中一個區間(活動區間),而另外一個區間(空間區間)則是空閑的。

複制算法在存活對象比較少的時候,極為高效,但是帶來的成本是犧牲一半的記憶體空間用于進行對象的移動。

4、标記 - 整理算法

标記-整理算法采用 标記-清除 算法一樣的方式進行對象的标記、清除,但在回收不存活的對象占用的空間後,會将所有存活的對象往左端空閑空間移動,并更新對應的指針。标記-整理 算法是在标記-清除 算法之上,又進行了對象的移動排序整理,是以成本更高,但卻解決了記憶體碎片的問題。

垃圾回收器簡介

1、Serial(-XX:+UseSerialGC)

從名字我們可以看出,這是一個串行收集器。Serial收集器是Java虛拟機中最基本、曆史最悠久的收集器。在JDK1.3之前是Java虛拟機新生代收集器的唯一選擇。目前也是ClientVM下ServerVM 4核4GB以下機器預設垃圾回收器。Serial收集器并不是隻能使用一個CPU進行收集,而是當JVM需要進行垃圾回收的時候,需暫停所有的使用者線程,直到回收結束。

使用算法:複制算法

JVM垃圾回收算法

2、SerialOld(-XX:+UseSerialGC)

SerialOld是Serial收集器的老年代收集器版本,它同樣是一個單線程收集器,這個收集器目前主要用于Client模式下使用。如果在Server模式下,它主要還有兩大用途:一個是在JDK1.5及之前的版本中與Parallel Scavenge收集器搭配使用,另外一個就是作為CMS收集器的後備預案,如果CMS出現Concurrent Mode Failure,則SerialOld将作為後備收集器。

使用算法:标記 - 整理算法

3、ParNew(-XX:+UseParNewGC)

ParNew其實就是Serial收集器的多線程版本。除了Serial收集器外,隻有它能與CMS收集器配合工作。

JVM垃圾回收算法

4、ParallelScavenge(-XX:+UseParallelGC)

ParallelScavenge又被稱為吞吐量優先收集器,和ParNew 收集器類似,是一個新生代收集器。

ParallelScavenge收集器的目标是達到一個可控件的吞吐量,所謂吞吐量就是CPU用于運作使用者代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運作使用者代碼時間 / (運作使用者代碼時間 + 垃圾收集時間)。如果虛拟機總共運作了100分鐘,其中垃圾收集花了1分鐘,那麼吞吐量就是99% 。

5、ParallelOld(-XX:+UseParallelOldGC)

ParallelOld是并行收集器,和SerialOld一樣,ParallelOld是一個老年代收集器,是老年代吞吐量優先的一個收集器。這個收集器在JDK1.6之後才開始提供的,在此之前,ParallelScavenge隻能選擇SerialOld來作為其老年代的收集器,這嚴重拖累了ParallelScavenge整體的速度。而ParallelOld的出現後,“吞吐量優先”收集器才名副其實!

6、CMS (-XX:+UseConcMarkSweepGC)

CMS(Concurrent Mark-Sweep)是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器,是一個老年代收集器,全稱 Concurrent Low Pause Collector,是JDK1.4後期開始引用的新GC收集器,在JDK1.5、1.6中得到了進一步的改進。它是對于響應時間的重要性需求大于吞吐量要求的收集器。對于要求伺服器響應速度高的情況下,使用CMS非常合适。

CMS的一大特點,就是用兩次短暫的暫停來代替串行或并行标記整理算法時候的長暫停。

7、GarbageFirst(G1)

這是一個新的垃圾回收器,既可以回收新生代也可以回收老年代,SunHotSpot1.6u14以上EarlyAccess版本加入了這個回收器,Sun公司預期SunHotSpot1.7釋出正式版本。通過重新劃分記憶體區域,整合優化CMS,同時注重吞吐量和響應時間。杯具的是Oracle收購這個收集器之後将其用于商用收費版收集器。是以目前暫時沒有發現哪個公司使用它,這個放在之後再去研究吧。

新生代收集器:

Serial (-XX:+UseSerialGC)

ParNew(-XX:+UseParNewGC)

ParallelScavenge(-XX:+UseParallelGC)

G1 收集器

老年代收集器:

SerialOld(-XX:+UseSerialOldGC)

ParallelOld(-XX:+UseParallelOldGC)

CMS(-XX:+UseConcMarkSweepGC)

CMS使用場景:GC過程短暫停,适合對時延要求較高的服務,使用者線程不允許長時間的停頓

觸發條件

1、如果沒有設定-XX:+UseCMSInitiatingOccupancyOnly,虛拟機會根據收集的資料決定是否觸發(建議線上環境帶上這個參數,不然會加大問題排查的難度)。

2、老年代使用率達到門檻值 

CMSInitiatingOccupancyFraction

,預設92%。

3、永久代的使用率達到門檻值 

CMSInitiatingPermOccupancyFraction

,預設92%,前提是開啟 

CMSClassUnloadingEnabled

4、新生代的晉升擔保失敗。

晉升擔保失敗

老年代是否有足夠的空間來容納全部的新生代對象或曆史平均晉升到老年代的對象,如果不夠的話,就提早進行一次老年代的回收,防止下次進行YGC的時候發生晉升失敗。

CMS的執行過程如下:

· 初始标記(STW initial mark)

在這個階段,需要虛拟機停頓正在執行的應用線程,官方的叫法STW(Stop Tow World)。這個過程從根對象掃描直接關聯的對象,并作标記。這個過程會很快的完成。

· 并發标記(Concurrent marking)

這個階段緊随初始标記階段,在“初始标記”的基礎上繼續向下追溯标記。注意這裡是并發标記,表示使用者線程可以和GC線程一起并發執行,這個階段不會暫停使用者的線程哦。

· 并發預清理(Concurrent precleaning)

這個階段依舊是并發的,JVM查找正在執行“并發标記”階段時候進入老年代的對象(可能這時會有對象從新生代晉升到老年代,或被配置設定到老年代)。通過重新掃描,減少在一個階段“重新标記”的工作,因為下一階段會STW。

· 重新标記(STW remark)

這個階段會再次暫停正在執行的應用線程,重新從根對象開始查找并标記并發階段遺漏的對象(在并發标記階段結束後對象狀态的更新導緻),并處理對象關聯。這一次耗時會比“初始标記”更長,并且這個階段可以并行标記。

· 并發清理(Concurrent sweeping)

這個階段是并發的,應用線程和GC清除線程可以一起并發執行。

· 并發重置(Concurrent reset)

這個階段依舊是并發的,重置CMS收集器的資料結構,等待下一次垃圾回收。

JVM垃圾回收算法

CMS的缺點:

1、記憶體碎片。由于使用了标記-清理 算法,導緻記憶體空間中會産生記憶體碎片。不過CMS收集器做了一些小的優化,就是把未配置設定的空間彙總成一個清單,當有JVM需要配置設定記憶體空間的時候,會搜尋這個清單找到符合條件的空間來存儲這個對象。但是記憶體碎片的問題依然存在,如果一個對象需要3塊連續的空間來存儲,因為記憶體碎片的原因,尋找不到這樣的空間,就會導緻Full GC。

2、需要更多的CPU資源。由于使用了并發處理,很多情況下都是GC線程和應用線程并發執行的,這樣就需要占用更多的CPU資源,也是犧牲了一定吞吐量的原因。

3、需要更大的堆空間。因為CMS标記階段應用程式的線程還是執行的,那麼就會有堆空間繼續配置設定的問題,為了保障CMS在回收堆空間之前還有空間配置設定給新加入的對象,必須預留一部分空間。CMS預設在老年代空間使用92%時候啟動垃圾回收。可以通過-XX:CMSinitiatingOccupancyFraction=n來設定這個閥值。