GC(一)內存管理與垃圾回收

參考文章:html

內存分配、GC原理與垃圾收集器:http://www.importnew.com/23035.htmljava

g1垃圾回收器:http://blog.jobbole.com/109170/算法

cms垃圾回收器:http://www.cnblogs.com/littleLord/p/5380624.html數組

1、怎樣分配- JVM內存分配策略

對象內存主要分配在新生代Eden區, 若是啓用了本地線程分配緩衝, 則優先在TLAB上分配, 少數狀況能會直接分配在老年代, 或被拆分紅標量類型在棧上分配(JIT優化). 分配的規則並非百分百固定, 細節主要取決於垃圾收集器組合, 以及VM內存相關的參數安全

對象分配
       優先在Eden區分配
       在JVM內存模型一文中, 咱們大體瞭解了VM年輕代堆內存能夠劃分爲一塊Eden區和兩塊Survivor區. 在大多數狀況下, 對象在新生代Eden區中分配, 當Eden區沒有足夠空間分配時, VM發起一次Minor GC, 將Eden區和其中一塊Survivor區內尚存活的對象放入另外一塊Survivor區域, 若是在Minor GC期間發現新生代存活對象沒法放入空閒的Survivor區, 則會經過空間分配擔保機制使對象提早進入老年代。服務器

       空間分配擔保機制在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗。若是容許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,儘管此次Minor GC是有風險的;若是小於,或者HandlePromotionFailure設置不容許冒險,那這時也要改成進行一次Full GC。
       大對象直接進入老年代
       Serial和ParNew兩款收集器提供了-XX:PretenureSizeThreshold的參數, 令大於該值的大對象直接在老年代分配, 這樣作的目的是避免在Eden區和Survivor區之間產生大量的內存複製(大對象通常指 須要大量連續內存的Java對象, 如很長的字符串和數組), 所以大對象容易致使還有很多空閒內存就提早觸發GC以獲取足夠的連續空間.多線程

對象晉升
       年齡閾值
       VM爲每一個對象定義了一個對象年齡(Age)計數器, 對象在Eden出生若是經第一次Minor GC後仍然存活, 且能被Survivor容納的話, 將被移動到Survivor空間中, 並將年齡設爲1. 之後對象在Survivor區中每熬過一次Minor GC年齡就+1. 當增長到必定程度(-XX:MaxTenuringThreshold, 默認15), 將會晉升到老年代.
       提早晉升: 動態年齡斷定
       然而VM並不老是要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代: 若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半, 年齡大於或等於該年齡的對象就能夠直接進入老年代, 而無須等到晉升年齡併發

什麼時候回收-對象生死斷定
    在堆裏面存放着Java世界中幾乎全部的對象實例, 垃圾收集器在對堆進行回收前, 第一件事就是判斷哪些對象已死(可回收)jvm

  1. 可達性分析算法

在主流商用語言(如Java、C#)的主流實現中, 都是經過可達性分析算法來斷定對象是否存活的: 經過一系列的稱爲 GC Roots 的對象做爲起點, 而後向下搜索; 搜索所走過的路徑稱爲引用鏈/Reference Chain, 當一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達, 也就說明此對象是不可用的, 以下圖: Object五、六、7 雖然互有關聯, 但它們到GC Roots是不可達的, 所以也會被斷定爲可回收的對象:

    在Java, 可做爲GC Roots的對象包括:性能

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

    注: 即便在可達性分析算法中不可達的對象, VM也並非立刻對其回收, 由於要真正宣告一個對象死亡, 至少要經歷兩次標記過程:

第一次是在可達性分析後發現沒有與GC Roots相鏈接的引用鏈

第二次是GC對在F-Queue執行隊列中的對象進行的小規模標記(對象須要覆蓋finalize()方法且沒被調用過)

       2.引用計數法

        堆中每一個對象實例都有一個引用計數。當一個對象被建立時,且將該對象實例分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例能夠被看成垃圾收集

2、 垃圾回收機制中的算法

一、引用計數法(Reference Counting Collector)

算法分析
     引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每一個對象實例都有一個引用計數。當一個對象被建立時,且將該對象實例分配給一個變量,該變量計數設置爲1。當任何其它變量被賦值爲這個對象的引用時,計數加1(a = b,則b引用的對象實例的計數器+1),但當一個對象實例的某個引用超過了生命週期或者被設置爲一個新值時,對象實例的引用計數器減1。任何引用計數器爲0的對象實例能夠被看成垃圾收集。當一個對象實例被垃圾收集時,它引用的任何對象實例的引用計數器減1
優勢
    引用計數收集器能夠很快的執行,交織在程序運行中。對程序須要不被長時間打斷的實時環境比較有利
缺點
    沒法檢測出循環引用。如父對象有一個對子對象的引用,子對象反過來引用父對象。這樣,他們的引用計數永遠不可能爲0

2)tracing算法(Tracing Collector) 或 標記-清除算法(mark and sweep)

算法分析

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

優勢

      標記-清除算法不須要進行對象的移動,而且僅對不存活的對象進行處理,在存活對象比較多的狀況下極爲高效

缺點

      效率問題: 標記和清除過程的效率都不高
      空間問題: 標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會致使在運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集

三、compacting算法 或 標記-整理算法

算法分析

      標記-整理算法採用標記-清除算法同樣的方式進行對象的標記,但在清除時不一樣,在回收不存活的對象佔用的空間後,會將全部存活的對象都向一端移動,而後清理掉端邊界之外的內存

 

優勢:解決了內存碎片的問題

缺點:標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,所以成本更高

四、copying算法(Compacting Collector)

算法分析

      該算法的核心是將可用內存按容量劃分爲大小相等的兩塊, 每次只用其中一塊, 當這一塊的內存用完, 就將還存活的對象複製到另一塊上面, 而後把已使用過的內存空間一次清理掉。在兩塊切換過程當中,程序暫停執行

優勢:

      實現簡單,運行高效,沒有內存碎片

      現代商用VM的新生代均採用複製算法, 但因爲新生代中的98%的對象都是生存週期極短的, 所以並不需徹底按照1∶1的比例劃分新生代空間, 而是將新生代劃分爲一塊較大的Eden區和兩塊較小的Survivor區(HotSpot默認Eden和Survivor的大小比例爲8∶1), 每次只用Eden和其中一塊Survivor. 當發生MinorGC時, 將Eden和Survivor中還存活着的對象一次性地拷貝到另一塊Survivor上, 最後清理掉Eden和剛纔用過的Survivor的空間. 當Survivor空間不夠用(不足以保存尚存活的對象)時, 須要依賴老年代進行空間分配擔保機制, 這部份內存直接進入老年代.

3、分代收集算法 VS 分區收集算法

  • 分代收集

        當前主流VM垃圾收集都採用」分代收集」(Generational Collection)算法, 這種算法會根據對象存活週期的不一樣將內存劃分爲幾塊, 如JVM中的 新生代、老年代、永久代. 這樣就能夠根據各年代特色分別採用最適當的GC算法:
        在新生代: 每次垃圾收集都能發現大批對象已死, 只有少許存活. 所以選用複製算法, 只須要付出少許存活對象的複製成本就能夠完成收集
        在老年代: 由於對象存活率高、沒有額外空間對它進行分配擔保, 就必須採用「標記—清理」或「標記—整理」算法來進行回收, 沒必要進行內存複製, 且直接騰出空閒內存

  • 分區收集

        上面介紹的分代收集算法是將對象的生命週期按長短劃分爲兩個部分, 而分區算法則將整個堆空間劃分爲連續的不一樣小區間, 每一個小區間獨立使用, 獨立回收. 這樣作的好處是能夠控制一次回收多少個小區間
        在相同條件下, 堆空間越大, 一次GC耗時就越長, 從而產生的停頓也越長. 爲了更好地控制GC產生的停頓時間, 將一塊大的內存區域分割爲多個小塊, 根據目標停頓時間, 每次合理地回收若干個小區間(而不是整個堆), 從而減小一次GC所產生的停頓

4、GC實現- 垃圾收集器

分代收集-新生代

  •  Serial收集器

         Serial收集器是Hotspot運行在Client模式下的默認新生代收集器, 它的特色是 只用一個CPU/一條收集線程去完成GC工做, 且在進行垃圾收集時必須暫停其餘全部的工做線程   (「Stop The World」 -後面簡稱STW).
         雖然是單線程收集, 但它卻簡單而高效, 在VM管理內存不大的狀況下(收集幾十M~一兩百M的新生代), 停頓時間徹底能夠控制在幾十毫秒~一百多毫秒內.

  • ParNew收集器

      ParNew收集器實際上是前面Serial的多線程版本, 除使用多條線程進行GC外, 包括Serial可用的全部控制參數、收集算法、STW、對象分配規則、回收策略等都與Serial徹底同樣(也是VM啓用CMS收集器-XX: +UseConcMarkSweepGC的默認新生代收集器). 
     因爲存在線程切換的開銷, ParNew在單CPU的環境中比不上Serial, 且在經過超線程技術實現的兩個CPU的環境中也不能100%保證能超越Serial. 但隨着可用的CPU數量的增長, 收集效率確定也會大大增長(ParNew收集線程數與CPU的數量相同, 所以在CPU數量過大的環境中, 可用-XX:ParallelGCThreads參數控制GC線程數).

  • Parallel Scavenge收集器

        與ParNew相似, Parallel Scavenge也是使用複製算法, 也是並行多線程收集器. 但與其餘收集器關注儘量縮短垃圾收集時間不一樣, Parallel Scavenge更關注系統吞吐量:
         系統吞吐量=運行用戶代碼時間(運行用戶代碼時間+垃圾收集時間)
         停頓時間越短就越適用於用戶交互的程序-良好的響應速度能提高用戶的體驗;而高吞吐量則適用於後臺運算而不須要太多交互的任務-能夠最高效率地利用CPU時間,儘快地完成程序的運算任務

Parallel Scavenge提供了以下參數設置系統吞吐量:
      MaxGCPauseMillis     (毫秒數) 收集器將盡力保證內存回收花費的時間不超過設定值, 但若是過小將會致使GC的頻率增長.
      GCTimeRatio     (整數:0 < GCTimeRatio < 100) 是垃圾收集時間佔總時間的比率
      -XX:+UseAdaptiveSizePolicy     啓用GC自適應的調節策略: 再也不須要手工指定-Xmn、-XX:SurvivorRatio、-XX:PretenureSizeThreshold等細節參數, VM會根據當前系統的運行狀況收集性能監控信息, 動態調整這些參數以提供最合適的停頓時間或最大的吞吐量

方法區的垃圾回收

方法區的垃圾回收主要回收兩部份內容:1. 廢棄常量。2. 無用的類。
如何判斷廢棄常量呢?以字面量回收爲例,若是一個字符串「abc」已經進入常量池,可是當前系統沒有任何一個String對象引用了叫作「abc」的字面量,那麼,若是發生垃圾回收而且有必要時,「abc」就會被系統移出常量池。常量池中的其餘類(接口)、方法、字段的符號引用也與此相似。
如何判斷無用的類呢?須要知足如下三個條件

  1.  該類的全部實例都已經被回收,即Java堆中不存在該類的任何實例
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法

知足以上三個條件的類能夠進行垃圾回收,可是並非無用就被回收,虛擬機提供了一些參數供咱們配置

分代收集-老年代

  • Serial Old收集器

       Serial Old是Serial收集器的老年代版本, 一樣是單線程收集器,使用「標記-整理」算法:
       Serial Old應用場景以下:
        JDK 1.5以前與Parallel Scavenge收集器搭配使用;
        做爲CMS收集器的後備預案, 在併發收集發生Concurrent Mode Failure時啓用(見下:CMS收集器).

  • Parallel Old收集器

     Parallel Old是Parallel Scavenge收老年代版本, 使用多線程和「標記-整理」算法, 吞吐量優先, 主要與Parallel Scavenge配合在 注重吞吐量 及 CPU資源敏感 系統內使用:

  • CMS收集器

   CMS(Concurrent Mark Sweep)收集器是一款具備劃時代意義的收集器, 一款真正意義上的併發收集器, 雖然如今已經有了理論意義上表現更好的G1收集器, 但如今主流互聯網企業線上選用的還是CMS(如Taobao、微店).
CMS是一種以獲取最短回收停頓時間爲目標的收集器(CMS又稱多併發低暫停的收集器), 基於」標記-清除」算法實現, 整個GC過程分爲如下4個步驟:
1. 初始標記(CMS initial mark)
2. 併發標記(CMS concurrent mark: GC Roots Tracing過程)
3. 從新標記(CMS remark)
4. 併發清除(CMS concurrent sweep: 已死象將會就地釋放, 注意: 此處沒有壓縮)
其中兩個步驟(初始標記、從新標記)仍需STW. 但初始標記僅只標記一下GC Roots能直接關聯到的對象, 速度很快; 而從新標記則是爲了修正併發標記期間因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄, 雖然通常比初始標記階段稍長, 但要遠小於併發標記時間

因爲整個GC過程耗時最長的併發標記和併發清除階段的GC線程可與用戶線程一塊兒工做, 因此整體上CMS的GC過程是與用戶線程一塊兒併發地執行的.
因爲CMS收集器將整個GC過程進行了更細粒度的劃分, 所以能夠實現併發收集、低停頓的優點, 但它也並不是十分完美, 其存在缺點及解決策略以下:

    • CMS默認啓動的回收線程數=(CPU數目+3)/4.當CPU數>4時, GC線程最多佔用不超過25%的CPU資源, 可是當CPU數<=4時, GC線程可能就會過多的佔用用戶CPU資源, 從而致使應用程序變慢, 總吞吐量下降.
    • 沒法處理浮動垃圾, 可能出現Promotion Failure、Concurrent Mode Failure而致使另外一次Full GC的產生: 浮動垃圾是指在CMS併發清理階段用戶線程運行而產生的新垃圾. 因爲在GC階段用戶線程還需運行, 所以還須要預留足夠的內存空間給用戶線程使用, 致使CMS不能像其餘收集器那樣等到老年代幾乎填滿了再進行收集. 所以CMS提供了-XX:CMSInitiatingOccupancyFraction參數來設置GC的觸發百分比(以及-XX:+UseCMSInitiatingOccupancyOnly來啓用該觸發百分比), 當老年代的使用空間超過該比例後CMS就會被觸發(JDK 1.6以後默認92%). 但當CMS運行期間預留的內存沒法知足程序須要, 就會出現上述Promotion Failure等失敗, 這時VM將啓動後備預案: 臨時啓用Serial Old收集器來從新執行Full GC(CMS一般配合大內存使用, 一旦大內存轉入串行的Serial GC, 那停頓的時間就是你們都不肯看到的了).
    • 因爲CMS採用」標記-清除」算法實現, 可能會產生大量內存碎片. 內存碎片過多可能會致使沒法分配大對象而提早觸發Full GC. 所以CMS提供了-XX:+UseCMSCompactAtFullCollection開關參數, 用於在Full GC後再執行一個碎片整理過程. 但內存整理是沒法併發的, 內存碎片問題雖然沒有了, 但停頓時間也所以變長了, 所以CMS還提供了另一個參數-XX:CMSFullGCsBeforeCompaction用於設置在執行N次不進行內存整理的Full GC後, 跟着來一次帶整理的(默認爲0: 每次進入Full GC時都進行碎片整理).

分區收集-G1收集器

         G1(Garbage-First)是一款面向服務端應用的收集器, 主要目標用於配備多顆CPU的服務器治理大內存.
         - G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
         - -XX:+UseG1GC 啓用G1收集器.
        與其餘基於分代的收集器不一樣, G1將整個Java堆劃分爲多個大小相等的獨立區域(Region), 雖然還保留有新生代和老年代的概念, 但新生代和老年代再也不是物理隔離的了, 它們都 是一部分Region(不須要連續)的集合.

默認垃圾回收器

jdk1.7 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默認垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默認垃圾收集器G1

 4、gc實戰

一、如何查看jvm的垃圾回收狀況
 JVM的GC日誌的主要參數包括以下幾個:
-XX:+PrintGC 輸出GC日誌
-XX:+PrintGCDetails 輸出GC的詳細日誌
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行GC的先後打印出堆的信息
-Xloggc:../logs/gc.log 日誌文件的輸出路徑
二、簡單說下gc日誌

時間戳--新生代gc--gc前新生代大小--gc後新生代大小--新生代總大小--gc前堆大小--gc後堆大小--堆總大小--回收時間--用戶用時--系統耗時--實際gc時間

full gc以前會進行新生代younggc
三、若是沒有gc日誌如何查看?
jstat -gcutil -h 10 36403 1s

相關文章
相關標籤/搜索