歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

JVM垃圾收集器與內存分配策略

一、如何判斷對象是否還在存活

  • 引用計數法:

主流的Java虛擬機沒有使用這種方法管理內存, 因為它很難解決循環依賴

  • 可達性分析:

通過一系列的稱為”GC  Roots“的對象作為起始點, 從這些節點開始向下搜索, 搜索所走過的路徑稱為引用鏈, 當一個對象到GC Roots沒有與任何引用鏈相連時, 則證明該對象是不可用的。

作為GC Roots的對象包括以下幾種:虛擬機棧中引用的對象、 方法區中類靜態屬性引用的對象、方法區中常量引用的對象以及本地方法棧中JNI引用的對象。

二、引用:

  • 定義:

如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址, 就稱這塊內存代表著一個引用。

  • 分類:
  • 強引用:

代碼之中普遍存在的, 如Object obj = new Object(), 只要強引用還在, GC就永遠不會回收該內容。

  • 軟引用:

描述有用但非必須的對象。 對於軟引用關聯著的對象, 在系統將要拋出內存異常之前, 會將這些對象列進回收范圍進行二次回收。 如果這次回收還沒有足夠的內存, 才會拋出異常。(SoftReference)

  • 弱引用:

弱引用也用來描述非必須的對象。被若引用關聯的對象只能活到下次垃圾回收發生之前。 當垃圾收集器工作時, 無論當前內存是否足夠, 都會回收掉只被弱引用關聯的對象。

  • 虛引用:

又稱為幽靈引用或者幻影引用。 一個對象是否有虛引用的存在, 絲毫不會影響對象的生存時間, 也不能通過虛引用獲得對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被垃圾收集器回收時收到一個系統通知。

三、對象標記之後就會回收嗎

  • 可達性分析之後, 沒有在任何GC Roots引用鏈上的對象, 就會被第一次標記, 標記之後還有一次篩選的過程;
  • 篩選的條件是: 此對象是否有必要執行finalize方法,有必要執行的條件是:該對象覆蓋了finalize方法, 並且沒有虛擬機調用過, 也就是說任何一個對象的finalize方法都只會被系統執行一次。
  • 如果有必要執行finalize方法, 該對象則將會被放置一個成為F_Queue的隊列之中, 並在稍後由一個有虛擬機自動建立的、低優先級的Finalizer線程去觸發該方法。但不會等待finalize執行結束。
  • finalize方法是對象逃脫死亡命運的最後一次機會。稍後GC將對F_Queue中的對象進行第二次小規模的標記, 如果能與引用鏈上任何一個對象建立關系, 對象就不會被再次標記, 從而活下來。
  • 注:如果沒有必要執行finalize對象, 是不是就會立即被GC 回收呢

四、方法區的回收

  • 在堆中,尤其是新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間, 而永久代的垃圾收集效率遠低於此。
  • 永久代中的垃圾收集主要回收兩部分內容: 廢棄常量和無用的類
  • 廢棄常量: 當前系統中沒有任何一個對象引用該常量。
  • 無用的類: 該類所有的實例都已被回收(Java堆中不存在該類的任何實例)、加載該類的ClassLoader被回收、該類對應的java.lang.Class對象沒有在任何地方被引用, 無法在任何地方通過反射訪問該類的方法。

五、垃圾收集算法

1、標記-清除算法:

首先是標記出所有需要回收的對象, 在標記完成後回收所有被標記的對象。

缺點:

  • 效率問題: 標記和清楚兩個過程的效率都不高
  • 空間問題: 標記清除之後會造成大量不連續的內存碎片, 空間碎片太多導致需要分配較大對象時, 無法找到足夠的連續內存而不得不提前觸發另一次垃圾回收動作。

2、復制算法:

描述:將可用內存按容量劃分為兩塊, 每次只是用其中的一塊, 當這一塊內存用完了, 就將還存活的對象復制到另外一塊內存, 然後再把已使用過的內存空間一次性的清理掉。

優點:不用考慮內存碎片

缺點:內存縮小為原來的一半

應用:目前的商業虛擬機都采用這種收集算法來回收新生代,具體如下:

將內存分為一塊較大的Eden空間和兩塊較小的Survivior空間, 每次使用Eden和其中一個Survivior空間。

當回收時, 將Eden區和Survivior中還存活著的對象一次性的復制到另外一塊Survivior空間上, 最後清理Eden區和另一塊Survivior區。

Hotspot 虛擬機默認Eden和Survivior的大小比例是8:1

當另外一塊Survivior空間沒有足夠的空間存放上一次新生代存活的對象時, 這些對象將直接通過分配擔保機制進入老年代。

3、標記-整理算法:

標記之後, 讓所有存活的對象都向一端移動, 然後直接清理掉端邊界以外的內存。

4、分代收集算法:

當前虛擬機的垃圾收集都采用”分代收集“算法。一般是把Java堆分為新生代和老年代;

新生代: 每次垃圾收集時都會有大批對象死去, 只有少量存活, 該區域采用復制算法。

老年代:對象存活率較高, 而且沒有額外空間對她進行分配擔保, 就必須使用”標記-清理“或者”標記-整理“算法進行回收。

六、HotSpot 的算法

1、枚舉根節點:

GC停頓:

可達性分析要確保在一個一致性的快照中進行, 確保分析過程中引用關系不再變化。 GC進行時, 必須停頓所有的Java執行線程。 Stop the World

如何枚舉根節點:

GC Roots主要在全局性的引用, 如常量、類靜態屬性, 與執行上下文中, 如果逐個檢查, 必然消耗大量的時間。

使用OopMap: HotSpot使用一組OopMap來記錄對象的引用, 加快GC Roots的枚舉。

2、安全點:

背景:          如果每個指令都生成對應的OopMap, 那會需要大量的額外空間, 這樣GC的成本將會變得非常高。

解決辦法:    只在特定位置記錄對象的引用情況, 這些特點的位置我們稱之為安全點。

安全點的選定條件:是否具有讓程序長時間執行的指令 (原因)

如何保證GC時, 讓所有線程都跑到最近的安全點上再停頓下來:

  • 主動式中斷:

GC需要中斷線程的時候, 不直接對線程操作, 而是簡單的設置一個標志, 而是在執行到安全點時輪訓該標志, 如果標志為真就自己中斷掛起。

  • 搶先式中斷:

GC發生時, 首先把所有的線程全部中斷, 如果發現有線程中斷的地方不在安全點上, 就恢復線程, 讓他”跑“到安全點上。

現在幾乎沒有虛擬機采用這種方式來暫停線程以響應GC事件

3、安全區域:

背景: 安全點機制保證了程序執行時, 在不太長時間就會遇到可進入GC的安全點, 如果程序沒有執行呢, 比如出於sleep或者blocked狀態,

這時候線程無法響應jvm的中斷請求, Jvm也顯然不太可能等待線程被重新分配CPU時間。

安全區域:在一段代碼之中, 引用關系不會發生變化, 在這個區域中任意地方開始GC都是安全的

實現: 當線程執行到安全區域後, 首先會標示自己進入了安全區域, 那樣, 當在這段時間內發生GC時, 就不用管這樣的線程了,當線程要離開

該區域時, 要檢查系統是否已經完成了根節點枚舉, 如果沒完成, 它就必須等待直到收到可以安全離開安全區域的信號。

七,垃圾收集器

目前新生代垃圾收集器有Serial,ParNew,Parallel Scavenge; 老年代收集器有CMS,Serial Old,Parallel Old;G1這款垃圾收集器既能用於新生代又能用於老年代。

1,Serial收集器:

描述:    單線程收集器;他進行垃圾收集時,必須暫停所有的工作線程, 直到它收集結束;新生代采取復制算法暫停所有用戶線程, 老年代采取標記-整理算法暫停所有用戶線程。

現狀:    目前為止, 依然是虛擬機運行在Client模式下的默認新生代收集器。收集幾十兆甚至一兩百兆的新生代, 停頓時間完全可以控制在幾十毫秒最多一百毫秒以內。

2,ParNew收集器:

描述:    其實就是Serial的多線程版本;使用多線程進行垃圾收集;新生代采取復制算法暫停所有用戶線程,老年代采用標記-整理算法暫停所有用戶線程。

現狀:    許多運行在Server模式下的虛擬機默認的新生代收集器;一個與性能無關的原因是:除了Serial收集器外, 目前只有它能與CMS收集器配合使用。

注:

  • ParNew收集器是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器; 也可以使用-XX:UseParNewGC指定它
  • 可以使用-XX:+ParallelGCThreads參數限制垃圾收集的線程數

3,Parallel Scavenge收集器:

描述:    Parallel Scavenge收集器的目標是達到一個可控制的吞吐量。所謂吞吐量就是cpu用於運行用戶代碼的時間與CPU總消耗時間的比值, 即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)

參數:    最大垃圾收集停頓時間設置:-XX:MaxGCPauseMillis, 設置值是一個大於0的毫秒數, 收集器將盡可能地保證內存回收花費的時間不超過設定值。

GC停頓時間的縮短是以犧牲吞吐量和新生代空間來換取的,可能會把新生代調小一些, 以使在規定的時間內可以完成垃圾回收; 也可能為了減小停頓時間而增大GC頻率。

-XX:+GCTimeRatio:設置一個大於0且小於100的整數值, 也就是垃圾收集時間占總時間的比率,如果設置成x, GC時間的占比就是 1/(1+x)

-XX:+UseAdaptiveSizePolicy: 這是一個開關參數, 打開這個參數後, 不需要手工指定新生代大小,Eden和Survior區的比例,晉升老年代對象年齡等細節參數。 虛擬機會根據當前系統的運行情況動態調整這些參數以提供最合適的停頓時間或者最大吞吐量。

與ParNew收集器的區別:可以設置吞吐量和最大停頓時間; 具有自適應調節策略。

4,Serial Old收集器:

描述:Serial Old是Serial收集器的老年代版本, 單線程收集器, 使用“標記-整理”算法。

5,Parallel Old收集器:

描述: Parallel Scavenge收集器的老年代版本;��吐量優先的收集器

6,CMS收集器: Concurrent Mark Sweep

描述:以獲取最短回收停頓時間為目標;基於“標記-清除”算法

步驟:

  • 初始標記:標記GC Roots能直接關聯到的對象, 速度很快; 該階段需要stop the world
  • 並發標記:進行GC Roots trace, 並發進行
  • 重新標記:修正並發期間因為用戶程序繼續運行而導致變動的那一部分對象的標記記錄。
  • 並發清除:

缺點:

  • CMS收集器對CPU資源非常敏感, 它雖然不會導致用戶線程停頓, 但是由於占用了一部分cpu時間而導致應用變慢, 總吞吐量會降低。
  • CMS收集器無法處理浮動垃圾, 可能出現Concurrent Mode Failure失敗, 導致另一次full gc的產生。並發清理過程中, 用戶線程生成的垃圾不能被本次回收所清理, 只能等到下次GC時清理, 這部分垃圾稱為“浮動垃圾”; 每次垃圾清理時, 都要為用戶線程的運行留出內存空間, 所以不能等到沒有空間時才回收。如果CMS運行期間預留的內存空間無法滿足程序需要, 就會出出現一次“Concurrent Mode Failure”失敗, 這時虛擬機將啟動後背預案:臨時使用Serial Old收集器重新進行老年代的垃圾收集; 使用-XX:CMSInitiatingOccupancyFraction設置阈值
  • CMS是基於“標記-清理”算法實現的收集器, 意味著收集結束時會產生大量的空間碎片。 空間碎片過多時, 會因為無法找到足夠連續的內存而無法為大對象分配內存, 造成FULL GC

八、 理解GC日志

1.停頓類型:

[GC : minor GC, [Full GC: full GC

2. GC的位置:

[DefNew:Default New Generation      Serial收集器新生代

[ParNew:  Parallel 新生代

[PSYoungGen: Parallel Scanvenge收集器的新生代

[Tenured:  老年代

[Perm:  永久代

3.回收前後內存空間變化:

35592K -> 1814K(36288K): 回收前內存空間大小 -> 回收後內存空間大小(總的內存空間大小

九、內存分配與回收策略:

大的方向說, 對象主要分配在堆的新生代的Eden區上,如果啟動了本地線程分配緩沖, 將按線程優先在TLAB上分配。

1.對象優先在Eden上分配:

當Eden上沒有足夠的空間分配時, 虛擬機會發起一次Minor GC, 將Eden上和一個survivior上存活的對象復制到另外一個Survivor空間上, 如果另外一個Survivior空間上沒有足夠的空間, 將會將存活的對象直接移動到老年代, 如果老年代也沒有足夠的空間, 虛擬機將會發起一次Full GC, 如果Full GC之後還是放不下, 則會報OOM異常。

2.大對象會直接進入老年代:

虛擬機提供一個參數, -XX:PretenureSizeThreshould, 大於這個值的對象直接在老年代分配, 避免Eden區域Survivior區的來回復制。

3.長期存活的對象直接進入老年代:

虛擬機每個對象定義了一個對象年齡計算器, 每經過一次Minor GC, 對象年齡加一, 當對象年齡達到一定數時(默認15),將會晉升到老年代。 阈值設置參數:-XX:MaxTenurngThreshould

4.動態對象年齡判斷:

如果Survivior空間中相同年齡的對象的大小總和大於survivior空間的一半時, 大於等於該年齡的對象就可以直接進入老年代。

5.空間分配擔保:

准備Minor GC時, 虛擬機首先會檢查老年代的剩余空間是否大於新生代所有對象的總空間, 如果大於, 則可以進行Minor GC; 否則, 會去查看是否允許擔保失敗(HandlePromotionFailure), 如果不允許,虛擬機會直接發起一次Full GC; 如果允許, 虛擬機會去檢查老年代剩余空間是否大於歷次晉升到老年代對象的平均大小, 如果不大於, 則會發起一次Full GC; 如果大於, 則會發起Minor GC, 如果這時發現, 老年代沒有足夠空間來容納新生代晉升來的對象的總大小, 這時仍要觸發一次Full GC, 這個圈子繞的就有點大了。

十、Full GC 和Minor GC:

Minor GC: 發生在新生代, 速度比較快

Full GC: 發生在老年代, 一般都伴隨這一次Minor GC

Minor GC一般都要比Minor GC慢十倍以上,因為新生代采用復制算法, 速度比較快; 而老年代一般采用標記-清理/整理算法;

Copyright © Linux教程網 All Rights Reserved