Java高級進階 2 內存分配策略和垃圾收集

對象存活判斷算法

    引用計數法

    給對象添加引用計數器,每當一個地方引用該對象,計數器+1,引用失效,計時器-1。任何時刻對象引用計時器爲0時,該對象不可被使用。引用計數法實現簡單、效率高,但是主流虛擬機都爲採用該算法進行內存管理,因爲該算法無法解決對象直接互相循環引用的問題。

    可達性分析算法

    目前主流虛擬機採用的主流算法,通過可達性分析來判斷對象是否存活。可達性算法基本思路是提供一系列“GC Roots”的對象作爲起始點,從這些節點往下搜索,搜索對象引用鏈,當對象到GC Roots沒有任何引用鏈相連時,該對象不可用。

    

    java語言規範定義了以下幾種對象可以作爲GC Roots的對象:

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

    方法區中類靜態屬性引用的對象;

    方法區中常量引用的對象;

    本地方法棧中Native方法引用的對象;

java引用類型(4類)

    JDK1.2以後,java對引用類型進行了擴充,將引用分爲Strong Reference、Soft Reference、Weak Reference、Phantom Reference 4種,這4種引用強度依次逐漸減弱。

    強引用(Strong Reference):程序代碼普遍存在的,類似Object c = new Object(); 這類的引用,只要引用還在垃圾收集器永遠不會回收掉被引用對象。

    軟引用(Soft Reference):程序中有用但非必須的對象,對於軟引用對象,在虛擬機內存將要發生溢出時,會將軟引用對象列入回收範圍進行二次回收,如果此次回收還沒有足夠內存纔會拋出內存溢出異常。JDK 提供了java.lang.ref.SoftReference類來實現軟引用。

    弱引用(Weak Reference):也是用來描述非必須對象,比軟引用強度更弱一些,被弱引用關聯對象只能生存到下一次垃圾收集前。當垃圾收集器工作時,無論內存是否足夠都會回收掉只被弱引用關聯的對象。

    虛引用:最弱引用,對象是否關聯弱引用對其生命週期完全不構成影響,僅僅是對象被垃圾收集器回收時收到一個通知。

    利用弱引用/軟引用有效避免OOM

    轉載至:http://www.cnblogs.com/dolphin0520/p/3784171.html

    java 4種引用我們常用的是強引用,另外三種使用最多的就是軟引用和弱引用。在java應用程序中,可以使用軟引用來實現圖片緩存和網頁緩存避免大量圖片或網頁緩存導致OOM的情況。

    對象死亡(回收)過程

    一個對象真正死亡,至少要經歷兩次標記過程:

    1、第一次標記:沒有GC Roots引用鏈關聯,將被第一次標記

    2、篩選:按照對象是否有必要執行的finalize()方法。當對象沒有覆蓋finalize方法或finalize方法已經被調用過,則虛擬機認爲沒必要執行。如果判斷有必要執行finalize方法,繼續步驟3

    3、放置進F-Queue隊列:將需要執行finalize()對象放入F-Queue隊列,隨後JVM自動創建一個低優先級的FInalizer線程去執行,需要注意的是JVM僅是觸發finalize()方法,並不會等待finalize()方法執行完成。

    4、對F-Queue隊列對象再次標記

垃圾回收算法

標記-清除

    最基礎收集算法,算法分爲“標記”和“清除”兩部分,其他算法都是基於它的思路並對其不足進行改進而得到的。標記-清除算法主要存在兩個不足:效率問題,標記和清除過程效率都不高;空間問題,標記清除算法會大量產生不連續的內存空間,可能會導致創建較大對象時無法獲取足夠連續的內存而不得不提前觸發另一次垃圾收集。

複製算法

    複製算法設計目標是爲了解決回收效率問題。複製算法的思路是將內存分爲兩塊相同大小空間,每次只使用其中的一塊,這一塊內存使用完了,先將存活的對象複製到另一塊,然後把已使用空間一次性清除掉。複製算法實現簡單、運行高效,但是代價是犧牲一半內存。

    現在商業虛擬機都採用複製算法回收新生代。MinorGc回收過程:複製-清空-互換

  •     首先、把Eden和Form Survivor區域存活對象複製到To Survivor區域(如果對象年齡達到老年代則將對象複製到老年代區),同時將這些存活對象年齡+1。如果To Survivor區域內存不足就存放到老年代,默認年齡達到15便移動到老年代;
  •     然後、清空Eden和From Survivor區域內存;
  •     最後、From 和 To Survivor區域互換,To Survivor作爲下次Gc的From 區域。

    複製算法在對象存活率較高時會進行很多次複製,效率會變低。同時如果不希望內存利用率只有50%,需要額外的空間進行分配擔保,以應對被使用內存所有對象100%存活的極端場景。所有老年代區域通常不採用複製算法。

標記-整理

    根據老年代特點,提出了標記-整理算法。標記過程和標記-清除算法一致,區別是標記後不立即清除,而是讓存活對象向另一端移動,然後直接回收掉端邊界以爲的內存。

分代收集算法

    主流商業虛擬機都採用分代收集算法,將內存劃分爲不同年代區域,然後根據不同年代對象特點採用不同算法。對於對象死亡頻率高的新生代區域採用複製算法(Minor Gc),對於對象存活率高的老年代採用標記-整理算法。

 垃圾收集器(基於HotSpot)

     垃圾收集器是內存回收的具體實現,java虛擬機規範對垃圾收集器具體實現沒有做任何規定,不同廠商、不同版本的虛擬機提供的垃圾收集器可能會有較大差別。垃圾收收集器可以區分爲新生代收集器、老年代收集器以及G1收集器。目前主流虛擬機還是使用GMS和G1收集器。

    

新生代收集器

    Serial

    Serial是最基本的收集器(歷史最久遠),單線程收集器,採用複製算法。在進行垃圾收集時,比較暫停其他所有工作線程,直到收集結束。

    Serial收集器由於其在單線程環境的簡單和高效特性,其依舊是運行在Client模式下默認的收集器。

    ParNew收集器

    ParNew收集器是Serial的多線程版本,除了使用多線程進行垃圾收集外,其餘包含所有控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器一樣。需要注意ParNew收集器的多線程是指採用多線程進行垃圾收集,在執行收集任務時仍需要暫時所有用戶線程。ParNew設計目標是通過多線程收集提高CPU的使用率,儘可能縮短垃圾收集的執行時間,從而縮短垃圾收集時用戶線程停頓時間。

    ParNew收集器目前是大多數Sever模式虛擬機新生代首選的收集器,主要是因爲目前除了Serial收集外,ParNew收集器是唯一能與CMS收集器配合工作的收集器。

    ParNew執行示意圖:

    ParNew默認開啓收集線程與實際的CPU數量相同,可以控制設置參數-XX:ParallelGCThreads 來限制允許的收集線程數,可以根據服務器實際情況進行調整。

    Parallel Scavenge 收集器

    Parallel Scavenge 收集器也是新生代的多線程收集器,採用複製算法。和ParNew不同的是,Parallel Scavenge 收集器設計的目標是達到一個可控制的吞吐量。吞吐量是指CPU用於運行用戶代碼的時間與CPU總時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。

    停頓時間越短就適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量可以高效的利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。

    Parallel Scavenge收集器可以通過參數精確控制吞吐量:

    -XX:MaxGCPauseMills 大於0的毫秒數,控制最大垃圾收集停頓時間,收集器將盡可能保證垃圾回收花費時間不超過設定值。

    -XX:GCTimeTatio 0-100之間的整數,垃圾收集時間佔總時間比例,相當於吞吐量的倒數。例如設置爲19,則允許的最大GC時間就佔總時間的5%(1/(1+19)),默認值爲99。

    -XX:+UseAdaptiveSizePolicy 開關參數,打開這個參數後,就可以打開GC自適應的調節策略——就不需要手工指定新生代大小(-Xmn)、Eden區和Survivor區比例、晉升老年代對象大小等細節參數,虛擬機會根據當前系統運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

老年代收集器

    Serial Old 收集器

    Serial Old 是Serial收集器的老年代版本,單線程收集器,採用標記-整理算法。該收集器目前主要意義也是作爲Client模式下虛擬機使用。另外Serial Old收集器還是作爲CMS收集器的後備預案,在併發收集出現Concurrent Model Failure時使用。

    Parallel Old 收集器

    Parallel Scavenge 收集器老版本,使用多線程和標記-整理算法,和Parallel Scavenge收集器組成吞吐量優先的垃圾收集應用組合。

    CMS 收集器

    CMS收集器設計目標是獲取最短垃圾回收停頓時間爲目標,目前主流垃圾收集器之一,大多數互聯網和B/S服務端等客戶線程停頓時間敏感的虛擬機優選收集器。CMS收集器基於標記-清除算法實現,CMS垃圾收集執行整個過程分爲四個步驟:

  •     初始標記:僅僅是標記下GC Roots能關聯到的對象,速度很快。初始標記需要Stop The World。
  •     併發標記:GC Roots Tracing 過程,該過程和用戶線程併發執行。
  •     重新標記:修正併發標記過程用戶線程併發執行導致標記產生變動的標記記錄。重新標記需要Stop The World.
  •     併發清除:   清除標記後需要回收的內存,該過程和用戶線程併發執行。

   從CMS收集過程整體來看,需要Stop The World的過程消耗時間都很短,需要花費較多時間的併發標記和併發清除過程都是與用戶線程併發執行的,因此總體來說CMS收集器執行垃圾收集是和用戶線程併發執行的。

    CMS收集器通過犧牲吞吐量和新生代空間爲代價來儘可能縮短用戶線程停頓時間。但是CMS並不完美,還存在如下缺點:

  •     CPU資源敏感,CMS在併發階段雖然不會停頓用戶線程,但是會因爲佔用部分線程而導致應用變慢,總的吞吐量降低。CMS默認啓動的回收線程數=(CPU數量+3)/4。
  •     無法處理浮動垃圾,CMS在併發清理階段,用戶線程還在執行,自然會產生新的垃圾,這一部分垃圾出現在標記過程之後當次收集CMS無法處理,只能留待下次垃圾收集處理。因此CMS需要預留一部分空間提供併發收集時程序運作使用。CMS在JDK 1.6 以後啓動閾值默認當老年代使用達到92%。當垃圾收集時,CMS預留內存無法滿足程序需要,就會出現一次Concurrent Mode Failure,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器重新進行老年代的垃圾收集,此時停頓時間反而變長。因此啓動閾值設置過高將很容易出現Concurrent Mode Failure,性能反而降低。可以通過-XX:CMSInitiatiingOccupancyFraction 的值來設置觸發百分比。
  •     會產生大量空間碎片,CMS基於標記-清理算法,收集結束時會有大量空間碎片產生。這會給大對象分配帶來很大麻煩(根據內存分配策略,大對象將盡可能分配到老年代),如果無法找到足夠大的連續內存空間將會提前觸發一次Full GC。可以通過參數-XX:+UseCMSCompactAtFullCollection 開關參數(默認開啓) 啓動CMS收集在內存要頂不住進行Full GC時開啓內存合併整理過程(需要注意內存合併整理過程也是Stop The World的,解決空間碎片問題帶來的是用戶線程停頓時間的增長)。可以通過參數-XX:CMSFullGCsBeforeCompaction 設置執行多少次不整理空間的Full GC後,下一次Full GC同時啓動空間合併整理(默認0,每次Full GC都進行空間整理)。

    G1收集器

    JDK1.7 引入G1收集器,G1同樣關注最小時延同時適合大尺寸堆內存的收集器,G1是一款面向服務端應用的垃圾收集器,官方也推薦G1代替CMS。G1引入分區的思路,弱化分代概念,合理利用垃圾收集各個週期的資源,解決了其他收集器衆多缺點。

    G1收集器具有如下特點:

  •     並行與併發:G1能充分利用多CPU、多核環境的硬件優勢,使用多個CPU來縮短Stop The World停頓的時間。
  •     分代收集:G1雖然弱化了分代概念,但仍保留了分代概念。和其他收集器分代的概念不同,G1沒有了物理上年輕代和老年代的劃分,也不是需要完全獨立的Survivor堆區域做複製準備。G1只有邏輯上分代的概念,G1將整個堆劃分爲多個大小相同的獨立區域(Regin),每個區域都可能隨着G1運行進行切換。
  •     空間整合:與CMS標記-清除算法不同,G1從整體來說是基於標記-整理算法實現,從局部來看(兩個Regin之間)是基於複製算法,G1在運作過程都不會產生空間碎片。   
  •     可預測停頓:G1相對於CMS一大優勢就是能夠建立可預測的停頓時間模型,可以讓使用者明確指定在一個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。G1之所以可以建立可預測的停頓時間模型,是因爲G1可以有計劃的避免在整個java堆中進行全區域的垃圾收集。

    G1收集器運作過程大致可以分爲:

  •     初始標記:標記GC Roots 並修改TAMS(Next Top at Mark Start)的值,該步驟執行時間很短,需要Stop The World。
  •     併發標記:對象可達性分析,找出存活對象,併發標記耗時較長,但是與用戶線程併發執行。
  •     最終標記:修正併發標記過程因爲用戶線程執行導致標記狀態改變的標記記錄,這個階段需要停頓,但是可以並行執行收集任務。
  •     篩選回收:和其他回收期區別的是,G1收集器在回收階段不會一次性全部回收標記後的所有垃圾,而是先對所有Regin的回收價值和成本進行計算並排序,然後根據用戶所期望的停頓時間來制定回收計劃,從而達到可預估的停頓時間效果。

   垃圾收集器參數彙總

參  數 參數設置範圍 描述
-XX:+UseSerialGC 開關參數 打開後使用Serial+Serial Old的收集器組合進行內存回收
-XX:+UseParNewGC 開關參數 打開後使用ParNew + Serial Old收集器組合進行內存回收
-XX:+UseConcMarkSweepGC 開關參數 打開後使用ParNew + CMS + Serial Old收集器組合進行內存回收。
Serial  Old將作爲CMS 出現Concurrent Model Failure失敗後備選方案。
-XX:+UseParallelGC 開關參數 虛擬機運行在Server模型下的默認值。打開後使用Parallel Scavenge + Serial Old收集器組合進行內存回收
-XX:+UseParallelOldGC 開關參數 打開後使用Parallel Scavenge + Parallel Old組合進行垃圾回收
-XX:+UseG1GC 開關參數 打開後使用G1進行垃圾回收
-XX:SurvivorRatio 大於0整數 新生代Eden區域和Survivor區域容量比值,默認爲8,代表Eden:Survivo=8:1
-XX:PretenureSizeThreshold 大於0整數 設置直接晉升到老年代對象大小,單位k,當對象大小超過設值,直接進入老年代
注意:PretenureSizeThreshold參數僅對Serial和ParNew兩款收集器有效。
-XX:MaxTenuringThreshold 大於0整數 設置晉升到老年代對象年齡,默認15,大於這個值得對象將複製到老年代
-XX:+UseAdaptiveSizePolicy 開關參數 動態調整java堆中各區域的大小及進入老年代的對象年齡
-XX:HandlePromotionFailure 開關參數 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代整個Eden和Survivor區域對象都存活的極端情況。
-XX:ParallelGCThreads 大於0整數 設置並行收集時GC線程數
-XX:GCTimeRatio 0-100之間的整數 GC時間佔CPU總時間比率,默認99,即允許GC時間=1/(1+99),僅Parallel Scavenge收集器時有效
-XX:MaxGCPauseMills 大於0毫秒數 設置GC最大停頓時間,僅Parallel Scavenge收集器時有效
-XX:CMSInitiatingOccupancyFration 百分比數 設置CMS收集器在老年代內存使用多少後觸發垃圾收集,JDK1.7之前默認68%,JDK1.7及以後默認92%
-XX:+UseCMSCompactAtFullCollection 開關參數 默認開啓,設置CMS收集器在垃圾收集後會開啓空間整理
-XX:CMSFullGCsBeforeCompaction   設置CMS收集器在進行多少次不含空間整理的Full GC後開啓一次帶空間整理的Full GC,默認爲0——每次Full GC都進行空間整理
-Xloggc:eclipse_gc.log   打印GC詳細信息到日誌文件eclipse_gc.log
+PrintGCTimeStamps   打印GC時間信息
-XX:+PrintGCDetails   打印GC詳細信息
-XX:+HeapDumpOnOutOfMemoryError   讓虛擬機在出現內存溢出異常時Dump出當前的內存堆轉儲快照
XX:MaxDirectMemorySize=10M   設置直接內存區的最大容量爲10M
-XX:MaxPermSize=10M   設置方法區的最大容量爲10M
-XX:PermSize=10M   設置方法區的容量爲10M
-Xss128k   設置虛擬機棧的大小爲128k
-Xmn10M   設置新生代區的大小爲10M
-Xmx20M   設置堆最大容量爲20M
-Xms20M   設置堆最小容量爲20M
-verbose:gc   輸出虛擬機中GC的詳細情況

    內存分配策略

    三大分配原則+空間擔保:

  •     對象優先在Eden分配:大多數情況,對象在新生代Eden區中分配,Eden區內存不足夠分配對象空間,虛擬機將進行一次Minor GC
  •     大對象直接進入老年代:大量連續內存空間的對象直接分配在老年代,典型的就是很長的字符串以及數組。經常出現大對象容易導致內存還有不少空間時就提前觸發下一次垃圾收集以獲取連續空間來分配大對象內存。可以通過參數-XX:PretenureSizeThreshold設置直接進入老年代對象大小。程序開發過程尤其需要避免短命的大對象。
  •     長期存活對象進入老年代:虛擬機會給每個對象定義一個對象年齡(Age)計數器,每經過一次Minor GC後仍存活,並且能夠被Survivor區域分配的話,將被移動到Survivor區域,並且年齡+1。當Age增加到一定年齡(-XX:MaxTenuringThreshold=15 設置,默認15),將將會晉升到老年代。
  •     空間分配擔保:除了G1收集器,其他收集器組合,新生代收集都是採用複製算法,爲了利用率,只使用其中一個Survivor空間來作爲輪換備份,因此當出現大量對象在經過Mino GC後仍然存活的情況下,需要老年代進行分配擔保,前提是老年代還有剩餘空間容納這些對象。

 

    

    

   

    

 

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