天天看點

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

Java虛拟機知識點總結(二)

文章目錄

  • Java虛拟機知識點總結(二)
    • 1、垃圾收集器與配置設定政策
        • 1.1 對象是否死亡
        • 1.2 四種引用
        • 1.3 垃圾收集算法
        • 1.4 垃圾收集器

1、垃圾收集器與配置設定政策

1.1 對象是否死亡

​ 對堆垃圾回收前的第一步就是要判斷那些對象已經死亡(即不能再被任何途徑使用的對象) 。

1、引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加1;當引用失效,計數器就減1;任何時候計數器為0的對象就是不可能再被使用的。

這個方法實作簡單,效率高,但是目前主流的虛拟機中并沒有選擇這個算法來管理記憶體,其最主要的原因是它很難解決對象之間互相循環引用的問題。

public class Count {
    private Object instance;
    
    public Count() {
        // 占據20M記憶體
        byte[] m = new byte[20 * 1024 *1024];
    }
    
    public static void main(String[] args) {
        Count c1 = new Count();
        Count c2 = new Count();
        
        c1.instance = c2;
        c2.instance = c1;
        // 斷掉引用
        c1 = null;
        c2 = null;
        
        //垃圾回收
        System.gc();
    }
}
           

2、可達性分析法

此算法的核心思想:通過一系列稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為“引用鍊”,當一個對象到GC Roots沒有任何的引用鍊相連時(從GC Roots)到這個對象不可達)時,證明此對象不可用。

可作為GC Roots的對象包括下面幾種:

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

(2)方法區中類靜态屬性引用的對象

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

(4)本地方法棧中JNI(即一般說的Native方法)引用的對象。

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

1.2 四種引用

1、強引用

以前我們使用的大部分引用實際上都是強引用,類似于“Object obj = new Object()” ;這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空 間不足,Java虛拟機甯願抛出OutOfMemoryError錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足問題。

2、軟引用

如果一個對象隻具有軟引用,那就類似于可有可物的生活用品。如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些對象的記憶體。隻要垃圾回收器沒有回收它,該對象就可以被程式使用。軟引用可用來實作記憶體敏感的高速緩存。

3、弱引用

如果一個對象隻具有弱引用,那就類似于可有可物的生活用品。弱引用與軟引用的差別在于**:隻具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體**。不過,由于垃圾回收器是一個優先級很低的線程, 是以不一定會很快發現那些隻具有弱引用的對象。

4、虛引用

"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

4中引用執行個體示範

注意:虛引用主要用來跟蹤對象被垃圾回收的活動。

虛引用與軟引用和弱引用的一個差別在于: 虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的記憶體之前,把這個虛引用加入到與之關聯的引用隊列中。程式可以通過判斷引用隊列中是 否已經加入了虛引用,來了解被引用的對象是否将要被垃圾回收。程式如果發現某個虛引用已經被加入到引用隊列,那麼就可以在所引用的對象的記憶體被回收之前采取必要的行動。

特别注意,在程式設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速JVM對垃圾記憶體的回收速度,可以維護系統的運作安全,防止記憶體溢出(OutOfMemory)等問題的産生。

5、生存還是死亡

即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經曆兩次标記過程;可達性分析法中不可達的對象被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize方法。當對象沒有覆寫finalize方法,或finalize方法已經被虛拟機調用過時,虛拟機将這兩種情況視為沒有必要執行。被判定為需要執行的對象将會被放在一個隊列中進行第二次标記,除非這個對象與引用鍊上的任何一個對象建立關聯,否則就會被真的回收。

6、回收方法區

方法區(或Hotspot虛拟中的永久代)的垃圾收集主要回收兩部分内容:廢棄常量和無用的類。

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是 “無用的類” :

(1)該類所有的執行個體都已經被回收,也就是Java堆中不存在該類的任何執行個體。

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

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

1.3 垃圾收集算法

1、标記–清除算法

算法分為“标記”和“清除”階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。它是最基礎的收集算法。

會帶來兩個明顯的問題;

1、效率問題

2、空間問題(标記清除後會産生大量不連續的碎片)

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

2、複制算法(新生代)

為了解決效率問題,“複制”收集算法出現了。它可以将記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就将還存活的對象複制到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的記憶體回收都是對記憶體區間的一半進行回收。

現在的商業虛拟機都采用這種收集算法來回收新生代,而是将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,将Eden和Survivor中還存活着的對象一次性的複制到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛拟機預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體為整個新生代容量的90%(80%+10%),隻有10%的記憶體會被“浪費”。當Survivor空間不夠用時,需要依賴其他記憶體(這裡隻老年代)進行配置設定擔保。

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

3、标記–整理算法(老年代)

根據老年代的特點特出的一種标記算法,标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收對象回收,而是讓所有存活的對象向一段移動,然後直接清理掉端邊界以外的記憶體。

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

4、分代收集算法(HotSpot為什麼要分為新生代和老年代 )

目前虛拟機的垃圾手機都采用分代收集算法,這種算法沒有什麼新的思想,隻是根據對象存活周期的不同将記憶體分為幾塊。一般将java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合适的垃圾收集算法。

比如在新生代中,每次收集都會有大量對象死去,是以可以選擇複制算法,隻需要付出少量對象的複制成本就可以完成每次垃圾收集。而老年代的對象存活幾率是比較高的是以我們可以選擇“标記-清理”或“标記-整理”算法進行垃圾收集。

1.4 垃圾收集器

1、Serial 收集器

單線程垃圾收集器、最基本、發展最悠久。它的單線程的意義并不僅僅說明它隻會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。偶爾用在桌面應用中。 Serial收集器對于運作在Client模式下的虛拟機來說是個不錯的選擇。

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

2、ParNew 收集器

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其餘行為(控制參數、收集算法、回收政策等等)和Serial收集器完全一樣。

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

它是許多運作在Server模式下的虛拟機的首要選擇,除了Serial收集器外,隻有它能與CMS收集器(真正意義上的并發收集器,後面會介紹到)配合工作 。

兩個概念:

(1)并行:指多條垃圾收集線程并行工作,但此時使用者線程仍然處于等待狀态。

(2)并發:指使用者線程與垃圾收集線程同時執行(但不一定是并行,可能會交替執行),使用者程式在繼續運作,而垃圾收集器運作在另一個CPU上。

3、Paraller Scavenge 收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複制算法的收集器,又是并行的的多線程收集器。。。那麼它有什麼特别之處呢?

Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是使用者線程的停頓時間(提高使用者體驗)。所謂吞吐量就是CPU中用于運作使用者代碼的時間與CPU總消耗時間的比值。

-XX:MaxGCPauseMillis 垃圾收集器最大停頓的時間,但最大停頓時間過短必然會導緻新生代的記憶體大小變小,垃圾回收頻率變高,效率可能降低。

-XX:CGTIMERatio 吞吐量大小(0-100),預設為99。

4、Serial Old 收集器

Serial收集器的老年代版本,它同樣是一個單線程收集器,采用标記–整理算法。它主要有兩大用途:一種用途是在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的後備方案。

5、Parallel Old 收集器

Parallel Scavenge收集器的老年代版本。使用多線程和“**标記-整理”**算法。在注重吞吐量以及CPU資源的場合,都可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。

6、CMS 收集器(重點掌握)

CMS(Concurrent Mark Sweep)收集器是一種以擷取最短回收停頓時間為目标的收集器。它而非常符合在注重使用者體驗的應用上使用,目前很大一部分的java應用集中在網際網路站或者B/S系統的服務端上。

采用标記-清除算法,用于老年代,常與ParNew協同工作。優點在于并發收集與低停頓。 注:并行是指同一時刻同時做多件事情,而并發是指同一時間間隔内做多件事情 。

Java虛拟機垃圾收集器和配置設定政策Java虛拟機知識點總結(二)

四個步驟:

(1)初始标記:标記老年代中所有的GC Roots對象和年輕代中活着的對象引用到的老年代的對象,時間短;

(2)并發标記:從“初始标記”階段标記的對象開始找出所有存活的對象 ;

(3)重新标記:用來處理前一個階段因為引用關系改變導緻沒有标記到的存活對象,時間短;

(4)并發清除:清除那些沒有标記的對象并且回收空間。

三個明顯的缺陷:

(1)對CPU資源非常敏感

(2)無法處理浮動垃圾(由于CMS在并發清理過程中使用者還在運作,這過程産生的無法本次進行處理的垃圾城稱為浮動垃圾)

(3)空間碎片過多(因為其采用的是标記–清除算法)

7、G1 收集器(重點掌握)

G1 (Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器. 以極高機率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征。

四大特征:

(1)并行與并發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短 stop-The-World停頓時間。部分其他收集器原本需要停頓Java線程執行的GC動作,G1收集器仍然可以通過并發的 方式讓java程式繼續執行。

(2)分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。

(3)空間整合:與CMS的“标記–清理”算法不同,G1從整體來看是基于“标記–整理”算法實作的收集器;從局部上來看是基于“複制”算法實作的。

(4)可預測的停頓:這是G1相對于CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内。

G1之是以能夠建立可預測的停頓時間模型,是因為他可以有計劃地避免在整個Java堆進行全區域的掃描。G1跟蹤各個Region裡面的垃圾堆的價值大小,在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了GF收集器在有限時間内可以盡可能高的收集效率(把記憶體化整為零)。

四個步驟:

(1)初始标記:隻是标記一下GC Roots能直接關聯到的對象,并且修改TAMS的值,讓下一個階段使用者程式并發運作時,能在正确可用的Region中建立新對象 ,耗時短。

(2)并發标記:并發标記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段時耗時較長,但可與使用者程式并發執行 。

(3)最終标記:為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程Remenbered Set Logs裡面,最終标記階段需要把Remembered Set Logs的資料合并到Remembered Set Logs裡面, 這一階段需要停頓線程,但是可并行執行 。

(4)篩選回收:先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃 。

與CMS收集器對比:

(1)因為G1采用的是标記–整理算法,故不會産生碎片;

(2)因為G1在篩選回收階段,使用者不在運作,故不會産生浮動垃圾。

1.5 記憶體配置設定與回收政策

1、對象優先在Eden區配置設定

大多數情況下,對象在新生代中Eden區配置設定。當Eden區沒有足夠空間進行配置設定時,虛拟機将發起一次Minor GC

2、大對象直接進入老年代

大對象就是需要大量連續記憶體空間的對象(比如:字元串、數組)。

我們認為大對象不是朝生夕死的,如果放在新生代,則需要不斷移動,性能較差。

  • XX:PretenureSizeThreshold=6M 設定大檔案大小。

3、長期存活的對象将進入老年代

既然虛拟機采用了分代收集的思想來管理記憶體,那麼記憶體回收時就必須能識别那些對象應放在新生代,那些對象應放在老年代中。為了做到這一點,虛拟機給每個對象一個對象年齡(Age)計數器。

-XX:MaxTenuringThreshold最大年齡,預設為15; Age 1 + 1 + 1 使用年齡計數器

4、動态對象年齡判斷

為了更好的适應不同程式的記憶體情況,虛拟機不是永遠要求對象年齡必須達到了某個值才能進入老年代,如果Survivor 空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無需達到要求的年齡

1.6 空間配置設定擔保

-XX:+HandlePromotionFailure 開啟

-XX:-HandlePromotionFailure

禁用 取之前每一次回收晉升到老年代對象容量的平均值大小作為經驗值,與老年代的剩餘空間進行比較,決定是否FullGC來讓老年代騰出更多空間。

什麼情況下發生GC(when)

(1)Minor GC觸發的條件

1)新生代中Eden空間不足,對象優先在Eden中配置設定,當Eden中沒有足夠空間時,虛拟機将發生一次Minor GC,因為Java大多數對象都是朝生夕滅,是以Minor GC非常頻繁,而且速度也很快;

2)發生Minor GC時,虛拟機會檢測之前每次晉升到老年代的平均大小是否大于老年代的剩餘空間大小,如果大于,則進行一次Full GC,如果小于,則檢視HandlePromotionFailure設定是否允許擔保失敗,如果允許,那隻會進行一次Minor GC,如果不允許,則改為進行一次Full GC。

(2)Full GC觸發的條件

1)System.gc()方法的調用,此方法是建議JVM進行Full GC,雖然隻是建議而非一定,但很多情況下它會觸發 Full GC,進而增加Full GC的頻率,也即增加了間歇性停頓的次數。一般情況下不使用此方法,讓虛拟機自己去管理它的記憶體,可通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc()。

2) 老年代的記憶體空間不足,發生Full GC一般都會有一次Minor GC。大對象直接進入老年代,如很長的字元串數組,虛拟機提供一個-XX:PretenureSizeThreadhold參數,令大于這個參數值的對象直接在老年代中配置設定,避免在Eden區和兩個Survivor區發生大量的記憶體拷貝;

3)方法區記憶體空間不足,Permanet Generation中存放的為一些class的資訊、常量、靜态變量等資料,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被占滿,在未配置為采用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會抛出如下錯誤資訊:java.lang.OutOfMemoryError: PermGen space

為避免Perm Gen占滿造成Full GC現象,可采用的方法為增大Perm Gen空間或轉為使用CMS GC。

4)當Minor GC時,老年代的剩餘空間小于曆次從新生代往老年代中移的對象的平均記憶體空間大小時,Hotspot為了避免由于新生代對象晉升到舊生代導緻舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩餘空間,那麼就直接觸發Full GC。

例如程式第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大于6MB,如果小于6MB,則執行Full GC。

當新生代采用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大于6MB,如小于,則觸發對舊生代的回收。

對于使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可在啟動時通過- java -

Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

5)堆中配置設定很大的對象,大對象是指需要大量連續記憶體空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來配置設定給目前對象,此種情況就會觸發JVM進行Full GC。為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用于在“享受”完Full GC服務之後額外免費贈送一個碎片整理的過程,記憶體整理的過程無法并發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用于設定在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。

未完待續----------