1. 程式人生 > >深入理解Java虛擬機閱讀心得(三)

深入理解Java虛擬機閱讀心得(三)

暫停 計算 其他 分析 期望 時間 member 放下 提高

Java中提倡的自動內存管理最終可以歸結為自動化的解決兩個問題:

  1. 給對象分配內存
  2. 回收分配給對象的內存

先說說回收這一方面的兩個主要知識點

一。垃圾收集算法

1.標記-清理算法

  首先標記出所有需要回收的對象,然後在標記完成後統一回收所有被標記的對象(適用老年代)

  兩個缺陷:(1)效率問題,標記和清除兩個過程效率都不高

       (2)空間問題,標記清除算法會產生大量不連續的內存空間碎片,導致無法分配較大對象

2.復制算法

  將可用內存按容量劃分為等大小的兩塊,每次只使用其中的一塊。清理時將還存活著的對象復制到另一塊中,然後把已使用過的內存空間一次清理掉。這樣每次都是對整個半區進行回收。商業虛擬機中常用復制算收集新生代對象,但這種算法的代價內存使用率過低,為此對算法進行改進。

  將內存空間劃分為8:1:1的三個內存區間(Hotspot虛擬機中默認比例),其中占8比率的是Eden區,另外兩個是Survior區,每次使用Eden區和其中一個Survior區;清理時,將Eden區和Survior1區中還存活著的對象復制到Survior2區中,然後清理掉Eden區和Survior1區。此外,這裏在復制轉移對象時,當Survior2區的空間不夠用時,會需要依賴其他內存(老年代)進行分配擔保。

3.標記-整理算法

  首先標記處所有需要回收的對象,然後讓所有存活的對象向一端移動,最後直接清理掉端邊界以外的內存。(針對老年代特點)

4.分代收集算法

    根據對象存活周期的不同將內存劃分為幾塊。一般分為新生代(年輕代)和老年代(年老代),然後根據各個年代特點采用適當的收集算法。

二。垃圾收集器

  書中提到了總共7種不同的垃圾收集器,其中3種適用於年輕代,3種適用於年老代。此外,G1收集器為兩者通用

  (一)年輕代中的3種垃圾收集器(都是使用復制算法?):

  1.Serial收集器

    單線程的收集器(單線程,且在垃圾收集時必須暫停其他所有的工作線程,即回收停頓)。Client 場景下的默認新生代收集器。

     這種收集器簡單高效,無線程交互開銷,因此擁有最高的單線程收集效率;可以配合CMS或Serial Old使用

  2.ParNew收集器

     Serial收集器的多線程版本。Server模式下的虛擬機中首選的新生代收集器(因為只有Serial和ParNew能與CMS收集器配合)

     默認開啟的收集線程數與CPU的數量相同;可以配合CMS或Serial Old使用

  3.Parallel Scavenge收集器

     多線程收集器。與其他收集器目的不同,該收集器的目標是達到一個可控制的吞吐量(Throughput).

     吞吐量即CPU運行用戶代碼的時間和CPU總消耗時間的比值。可以配合Parallel Old或Serial Old使用

  (二)年老代中的三種垃圾收集器:

  1.Serial Old收集器

     Serial收集器的老年代版本,單線程收集器,使用標記-整理算法

  2.Parallel Old收集器

     Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法

  3.CMS(Concurrent Markup Sweap)收集器

    是一種以獲取最短回收停頓時間為目標的收集器。是基於標記-清理算法的。

    運行分為四個步驟:

      1.初始標記:標記 GC Roots 能直接關聯到的對象,需要回收停頓(即此時為單線程模式)

      2.並發標記:進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓

      3.重新標記:需要回收停頓。此階段是為了修正並發標記期間因用戶程序繼續運作而導致的標記變化部分

      4.並發清除

    CMS有三個明顯的缺點:

      1.對CPU資源非常敏感。CMS默認啟動的回收線程數量數為 (CPU數量+3)/4;即當CPU數量較少時,會分出較多比率的運算能力去執行多線程,即會導致吞吐量過低

      2.無法處理浮動垃圾。即並發清理過程中用戶線程運作產生的垃圾。當超過啟動閾值時(JDK1.6,閾值為92%),會出現"Concurrent Model Failure"而導致另一次Full GC產生。

      3.收集結束時會產生大量不連續的內存空間碎片。因為CMS是基於 標記-清除 算法的垃圾收集器。

      

   G1(Garbage-First)收集器

      G1 可以直接對新生代和老年代一起回收。G1收集器中,它將整個Java堆劃分成多個大小相等的獨立區域(Region),此時新生代和老年代不在是物理隔離的了,都是一部分Region的集合。

      通過引入 Region 的概念,從而將原來的一整塊內存空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。從而可以有計劃的在Java堆中進行全區域的垃圾收集,進而能夠建立可預測的停頓時間模型。G1通過跟蹤各個Region裏垃圾堆的價值大小,在後臺維護一個優先列表,每次優先回收價值最大的Region。通過使用Region劃分內存空間和優先級的區域回收方式,保證G1收集器在有效時間內獲得盡可能高的收集效率。此外,G1中的每個Region都有一個與之對應的Remembered Set用於記錄其他Region以及其他收集器中對該Region中對象的引用以避免虛擬機在做可達性分析的時避免全堆掃描

      不計算維護Remembered Set的操作,大致步驟如下:

      1.初始標記:需要停頓

      2.並發標記

      3.最終標記:為了修正在並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合並到 Remembered Set 中。需要停頓,但可以(多個最終標記線程)並發執行

      4.篩選標記:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。可與用戶程序一起並發執行,但停頓用戶線程將大幅度提高收集效率。

      G1收集器的四個特點

      1.並發與並行:通過利用多個CPU來縮短回收停頓時間,需要停頓的GC動作,也可通過並發的方式讓Java程序繼續執行

      2.分代收集:分代概念依然保留,無需其他收集器配合即可采用不同方式處理不同的生存周期對象

      3.空間整合整體上看是基於 標記-整理 算法,局部上(兩個Region)看是基於 復制 算法實現的。不會產生不連續的內存空間碎片

      4.可預測的停頓:除追求低停頓外,能建立可預測的停頓時間模型,並能讓使用者明確指定一個長度為M毫秒的時間片段。

再說說Java中如何自動化解決這兩個問題

三。內存分配和回收策略

  1.內存分配

    (1)對象優先在Eden區分配

      大多數情況下,對象在新生代Eden區分配,當Eden區內存空間不夠時,將觸發Minor GC

    (2)大對象直接進入老年代

      大對象指需要連續內存空間的對象,如很長的字符串和數組;經常出現大對象會提前觸發垃圾收集以獲取足夠長的連續內存空間放置大對象,因此可以通過設置參數來讓超過一定長度的大對象直接進入老年代。

    (3)長期存活的對象進入老年代

      為每個對象定義一個單獨的年齡計數器,每當該對象經過一次Minor GC之後存活,從Eden區(或survivor1區)復制到Survivor2區,則年齡+1;當對象的年齡增長到一定程度則進入老年代(默認年齡大於15的進入老年代)。

    (4)動態判斷對象年齡,滿足一定條件進入老年代

      JVM並不是一定要年齡到達一定程度才能進入老年代。當Survivor區中,某一年齡段的所有對象的大小和超過Survivor區的一半時,該年齡的所有對象都直接進入老年代。

  2.回收策略

    Minor GC:回收新生代。由於新生代對象存活時間短,因此Minor GC會頻繁執行,執行效率一般較快。  

    Full GC:回收老年代和新生代。由於老年代存活時間較長,因此Full GC很少執行,執行效率也比較慢。

    空間分配擔保:

      進行Minor GC前需要先檢查老年代的最大可用空間是否大於新生代所有對象的總空間,如果滿足,則認為Minor GC是安全的。

      如果不滿足,則虛擬機會查看是否允許擔保;如果允許,就會查看老年代最大可用連續內存空間是否大於以前每次晉升到老年代的對象的平均大小,大於,則開始Minor GC;如果不允許或小於,則觸發一次Full GC

      以HotSpot虛擬機為例,在Minor GC進行時,會將Eden區和Survivor1區的對象復制到Survivor2區中,若此時Survivor2區中的內存空間不足以放下所有的對象,此時會將多余的對象暫時存放在老年代區域。如果此時老年代區域不足以放置剩余對象,則會發生錯誤,並提前觸發Full GC。

  

深入理解Java虛擬機閱讀心得(三)