《深刻理解Java虛擬機》(三)垃圾收集器與內存分配策略

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

3.1 概述

本文參考的是周志明的 《深刻理解Java虛擬機》第三章 ,爲了整理思路,簡單記錄一下,方便後期查閱。java

3.2 對象已死嗎

在垃圾收集器進行回收前,第一件事就是肯定這些對象哪些還存活,哪些已經死去。算法

3.2.1 引用計數算法

在對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器減1;其中計數器爲0的對象是不可能再被使用的已死對象。segmentfault

  • 當兩個對象相互引用時,這兩個對象就不會被回收
  • 引用計數算法,不被主流虛擬機採用,主要緣由是它很難解決對象之間相互循環引用的問題。

3.2.2 可達性分析算法

經過一系列的稱爲GC Roots的對象做爲起始點,從這些節點開始向下搜索,搜索所通過
的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(在圖論中稱爲對象不可達)時,這個對象就是不可用的。數組

圖片來源於網絡若有侵權請私信刪除

圖片描述

在java語言中,可做爲GC Roots的對象包括:安全

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

3.2.3 引用的分類

java的引用能夠分爲強引用、軟引用、弱引用、虛引用:網絡

  • 強引用:是指在程序代碼中直接存在的引用,譬如引用new操做符建立的對象。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象
  • 軟引用:還有用可是並不是必需的引用,早系統將要發生內存溢出異常以前會把這些對象列進回收範圍中進行二次回收,若仍是沒有足夠的內存,纔會拋出內存溢出異常。
  • 弱引用:非必需的對象,只能生存到下一次垃圾收集發生以前。當垃圾收集器工做時,不管內存是否夠用都將回收這些對象。
  • 虛引用:一個對象是否有虛引用的存在徹底不會對他的生存時間構成影響,也沒法經過虛引用來取得一個對象實例。
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.2.4 宣告一個對象死亡的過程

要真正宣告一個對象死亡,至少要經歷兩次標記過程:數據結構

  • 若對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,會被 第一次標記 而且進行一次篩選。篩選的條件是此對象是否有必要執行finalize()方法(如當對象沒有重寫finalize()方法或者finalize()方法已經被虛擬機調用過則認爲沒有必要執行)。
  • 若是有必要執行則將該對象放置在F-Queue隊列中,並在稍後由一個由虛擬機本身創建的、低優先級的Finalizer線程去執行它;稍後GCF-Queue中的對象進行第二次標記,若是對象仍是沒有被引用,則會被回收。

可是做者不建議經過finalize()方法「拯救」對象,由於它運行代價高、不肯定性大、沒法保證各個對象的調用順序。多線程

圖片來源於網絡若有侵權請私信刪除

圖片描述

3.2.5 回收方法區

不少人認爲方法區(HotSopt中的永久代)是沒有垃圾收集的,java虛擬機規範中也沒有要求須要對方法區實現垃圾收集。併發

永久代(方法區)的垃圾收集主要回收兩部份內容: 廢棄常量和無用的類
  • 廢棄常量:假如一個字符串「abc」已經進入了常量池中,可是當前系統沒有任何一個String對象是叫 作「abc」的,換句話說,就是沒有任何String對象引用常量池中的「abc」常量,也沒有其餘 - 地方引用了這個字面量,若是這時發生內存回收,並且必要的話,這個「abc」常量就會被系 - 統清理出常量池。
  • 無用的類:同時知足下面3個條件的類(實例、類加載器被回收,java.lang.Class對象沒有被引用)。
  1. 該類全部的實例都已經被回收,也就是Java堆中不存在該類的任何實例
  2. 加載該類的ClassLoader已經被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

3.3 垃圾收集算法

3.3.1 標記-清除算法 (Mark-Sweep)

算法分爲兩個階段:標記和清除jvm

標記:首先標記全部須要回收的對象
清除:在標記完成後統一回收全部被標記的對象

標記過程在上文宣告一個對象死亡過程當中說起

缺點

  • 效率問題,標記和清除兩個過程的效率都不高(回收後空間碎片過多,再次回收(便可達性分析時)有時須要遍歷整個內存區域)。
  • 空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存,而不得不提早觸發另外一次垃圾收集動做。
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.3.2 複製算法(新生代算法)(Copying)

思路:將可用內存按容量分爲兩個塊,每次只用其中之一。當這一塊內存用完以後,將還存活的對象複製到另外一邊去,而後清除全部已經使用過的部分。

優勢

  • 每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效

缺點

  • 代價是將內存縮小爲了原來的一半,未免過高了一點。

解決方法

  • 新生代中的對象98%是「朝生夕死」的,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor
  • 在HotSpot裏,考慮到大部分對象存活時間很短將內存分爲Eden和兩塊Survivor,默認比例爲8:1:1。代價是存在部份內存空間浪費,適合在新生代使用。
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.3.3 標記-整理算法(老年代算法)(Mark-Compact)

標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

圖片來源於網絡若有侵權請私信刪除

圖片描述

3.3.4 分代收集算法

  • 當前商用虛擬機都採用了這種算法,根據對象的存活週期將內存劃分爲幾塊,通常是把Java堆分爲新生代和老生代,根據各個年代採用適當的收集算法
  • 新生代通常採用複製算法(Copying)
  • 老生代一搬採用 標記-清理(Mark-Sweep) 或者標記-整理(Mark-Compact) 進行回收。

3.4 hotspot的算法實現

3.4.1 枚舉根節點

可達性分析的缺點

GC Roots節點找引用鏈這個操做爲例,可做爲GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,如今不少應用僅僅方法區就有數百兆,若是要逐個檢查這裏面的引用,那麼必然會消耗不少時間。

因爲要確保在一致性的快照中進行可達性分析,從而致使GC進行時必需要停頓全部Java執行線程;

  • 目前主流的Java虛擬機使用的都是準確式GC,當執行系統停頓下來後並不須要一個不漏的檢查完全部執行上下文和全局的引用變量,虛擬機應當有辦法直接得知哪些地方存着對象的引用
  • HotSpot使用一組稱爲OopMap的數據結構**來記錄哪些地方存着對象的引用
  • 在類加載過程當中,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中會在特定的位置記錄下棧和寄存器中哪些位置是引用

判斷對象引用

  • 類加載時,使用OopMap的數據結構
  • JIT編譯時特定記錄

3.4.2 安全點

  • HotSpot沒有爲每條指令都生成OopMap,只是在特定位置記錄了這些信息,這些位置稱爲安全點。
  • 即程序執行時並不是在全部地方都能停頓下來開始GC,只有到達安全點時才能暫停。
  • 對於安全點基本上是以程序是否具備讓程序長時間執行的特徵(好比方法調用、循環跳轉、異常跳轉等)爲標準進行選定的。
  • 另外還須要考慮若是在GC時讓全部線程都跑到最近的安全點上,有兩種方案:搶先式中斷和主動式中斷;

搶先式中斷

  • 不須要線程的執行代碼主動去配合,在GC發生時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。 如今幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件

主動式中斷

  • GC須要中斷線程的時候,不直接對線程操做,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上建立對象須要分配內存的地方

二者的區別在於,搶先式中斷是不管如何都進行中斷,而主動式中斷則是線程執行輪詢標誌查看是否中斷

3.4.3 安全區域

  • 爲了處理不執行的程序的安全點問題,提出了安全區域來解決問題。
  • 安全區域是指在一段代碼片斷之中,引用關係不會發生變化,在這個區域內的任何地方進行GC都是安全的。
  • 虛擬機如個具體的進行內存回收是由虛擬機所採用的GC收集器決定的,而一般虛擬機中每每不止有一種GC收集器。
  • 線程執行到安全區域時,首先標識本身已經進入了安全區域,這樣JVMGC時就無論這些線程了。

3.5 垃圾收集器

  • 若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。
  • 不一樣的收集器應用的區域不一樣,到如今爲止沒有最好的收集器,也沒有萬能的收集器。

3.5.1 serial收集器

  • Serail 收集器是單線程的,他在進行垃圾收集時必須暫停其餘的全部線程,直到收集結束。
  • 隨着收集器的發展,用戶線程的停頓時間愈來愈段,但任然沒法消除。
  • Serial收集器是虛擬機運行在Client模式下默認的新生代收集器
  • 對於單個CPU壞境來講,Serial收集器**因爲沒有線程交互的開銷,專心作垃圾收集,能夠得到很高的單線程收集效率。
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.5.2 parnew收集器

  • ParNew收集器是Serial收集器的多線程版本
  • ParNew收集器是運行在Server模式下虛擬機中首選的新生代收集器
  • 在垃圾收集器中併發並行的概念:
  • 並行:多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
  • 併發:用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行在另外一個CPU上。
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.5.3 parallel scavenge收集器

  • 新生代收集器,使用複製算法,並行的多線程收集器;
  • 與其餘收集器關注於盡量縮短垃圾收集時用戶線程停頓時間不一樣,它的目標是達到一個可控制的吞吐量。

    • 吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
  • 高吞吐量能夠高效的利用CPU時間,儘快得完成程序的運算任務,主要適合在後臺運算而不須要太多交互的任務。
  • GC停頓時間的縮短是以犧牲吞吐量和新生代空間來換取的。
  • Parallel Scavenge收集器也常常被稱爲吞吐量優先收集器。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量

  • 控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數。
  • 直接設置吞吐量大小的-XX:GCTimeRatio參數。

3.5.4 serial old收集器

Serial Old是Serial收集器的老年代版本,它一樣是一個單線程收集器,使用「標記-整理」算法。

圖片來源於網絡若有侵權請私信刪除
圖片描述

3.5.5 parallel old收集器

Serial Old收集器是Serail收集器的老年代版本,是一個單線程收集器,使用標記-整理算法。

圖片來源於網絡若有侵權請私信刪除
圖片描述
  • Serail Old收集器主要用於Clinet模式下。
  • Serail Old收集器另外一種用途是做爲CMS收集器的後備預案。

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

圖片來源於網絡若有侵權請私信刪除
圖片描述

3.5.6 cms收集器

CMS收集器是一種以獲取最短的回收停頓時間爲目標的收集器。

CMS收集器基於標記-清楚算法實現,分爲四個步驟:初始標記、併發標記、從新標記、併發清除

步驟詳解

  • 初始標記:標記一下GC Roots能直接關聯到的對象,速度很快。
  • 併發標記:進行GC Roots Tracing
  • 從新標記:是爲了修正那些在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,在這一階段的停頓時間會比初始標記階段稍長一點。
  • 併發清除(CMS concurrent sweep)
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.5.7 g1收集器

G1收集器是一款面向服務端應用的垃圾收集器。
G1收集器具有如下特色:

並行與併發

  • G1能充分利用多CPU、 多核環境下的硬件優點,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。

分代收集

  • 與其餘收集器同樣,分代概念在G1中依然得以保留。 雖然G1能夠不須要其餘收集器配合就能獨立管理整個GC堆,但它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、 熬過屢次GC的舊對象以獲取更好的收集效果。

空間整合

  • 從總體上來看是基於「標記-整理」算法實現的,在局部上是基於複製算法實現的,但不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片,收集後能提供規整的可用內存。 這種特性有利於程序長時間運行,分配大對象時不會由於沒法找到連續內存空間而提早觸發下一次GC。

可預測的停頓

  • 這是G1相對於CMS的另外一大優點,下降停頓時間是G1CMS共同的關注點,但G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。

G1收集器將整個Java堆劃分爲多個大小相等的獨立區域,雖然還保留有新生代和老生代的概念,但新生代和老生代再也不是物理隔的了,他們是一部分Region的集合。

G1收集器能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集:跟蹤各個Region裏面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region

G1收集器中,使用Remembered Set來避免全堆掃描

G1收集器的運做大體可劃分爲如下幾個步驟:

初始標記(Initial Marking)

  • 僅僅只是標記一下GC Roots能直接關聯到的對象,而且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的Region中建立新對象,這階段須要停頓線程,但耗時很短。

併發標記(Concurrent Marking)

  • GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。

最終標記(Final Marking)

  • 爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線Remembered Set Logs裏面,最終標記階段須要把Remembered Set Logs的數據合併到Remembered Set中,這階段須要停頓線程,可是可並行執行。

篩選回收(Live Data Counting and Evacuation)

  • 首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃
圖片來源於網絡若有侵權請私信刪除
圖片描述

3.5.8 理解gc日誌

圖片來源於網絡若有侵權請私信刪除
圖片描述
  • 最前面的數字表明GC發生的時間(虛擬機啓動之後的秒殺)
  • 「[GC」「[Full GC」說明停頓類型,有Full表明的是Stop-The-World的;
  • 「[DefNew」「[Tenured」「[Perm」表示GC發生的區域;
  • 方括號內部的「3324K -> 152K(3712K)」 含義是 「GC前該內存已使用容量 -> GC後該內存區域已使用容量(該區域總容量)」;
  • 方括號以外的「3324K -> 152K(11904)」 含義是 「GC前Java堆已使用容量 -> GC後Java堆已使用容量(Java堆總容量)」;
  • 再日後「0.0025925 secs」表示該內存區域GC所佔用的時間;

3.5.9 垃圾收集器參數總結

垃圾收集器參數總結

-XX:+<option>啓用選項
-XX:-<option> 不啓用選項
-XX:<option>=<number> 
-XX:<option>=<string>

參數 描述
UserSerialGC 虛擬機在client模式下的默認值,打開此開關後,用於Serial+Serial Old的收集器組合進行內存回收
UserParNewGC 打開此開關 使用ParNew + Serial Old收集器組合進行內存回收
UseConcMarkSweepGC 打開此開關,使用ParNew+CMS+Serial Old收集器組合進行內存回收。Serial Old在CMS收集器出現concurrent Mode Failure 失敗後的後備收集器
UseParallelGC 在server模式下的默認值,打開此開關後使用Scavenge+Serial Old收集器組合進行回收
UseParallelOldGC 打開此開關後使用 Parallel Scavenge+Parallel Old收集器組合進行內存回收
SurvivorRatio 新生代中Eden區域與Survivor區域的比值,默認爲8,表示Eden:Survivor=8:1
PretenureSizeThreshold 直接晉升到老年代對象的大小,設置這個參數後大於這個參數的對象直接在老年代中分配
MaxTenuringThreshold 晉升老年代對象的年齡,每一個對象堅持一次MnorGC年齡就加一,當超過這個參數值就進入老年代
UseAdaptiveSizePolicy 動態調整java堆各個區域的大小以及進入老年代的年齡
HandlePromotionFailure 是否容許分配擔保失敗,即老年代剩餘空間不足以應付新生代整個對象都存活的特殊狀況
ParalleGCThreads 設置並行GC時進行內存回收的線程數
GCTimeratio GC時間佔總時間比率,默認值爲99,容許1%的GC時間。只在Parallel Seavenge收集器時生效
MaxGCPauseMillis 設置GC的最大停頓時間,只在Parallel Seavenge收集器時生效
CMSInitiatingOccupancyFration 設置CMS老年代空間被使用多少後觸發GC,默認值爲68%,只在CMS收集器時生效
UseCMSCompactAtFullCollection 設置CMS收集器完成垃圾收集後是否須要進行一次碎片整理,只在CMS垃圾收集器時生效
CMSFullGCBeforeCompaction 設置CMS收集器進行若干次垃圾收集後再啓動一次內存碎片整理,只在CMS垃圾收集器時生效

3.6 內存分配與回收策略

對象優先在新生代分配
大對象直接進入老年代
長期存活的對象將進入老年代

  • 動態對象年齡判斷:若是在Survivor空間中相同年齡全部對象大小總和大於Survivor空間的一半,大於或等於該年齡的對象直接進入老年代。
  • 空間分配擔保:發生Minor GC前,虛擬機會先檢查老年代最大可用連續空間是否大於新生代全部對象總空間,若是不成立,虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗,若是容許繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代的平均大小,若是大於會嘗試進行一次Minor GC;若是小於或者不容許冒險,會進行一次Full GC。

3.6.1 對象優先在eden分配

大多數狀況下,對象優先在新生代的Eden區分配。
當Eden區沒有足夠的空間時,虛擬機將發起一次Minor GC。
Minor GC與Full GC。

  • Minor GC:新生代GC,很是頻繁,回收速度快。
  • Fulll GC:老年代GC,又稱爲Major GC,常常會伴隨一次Minor GC,速度比較慢。

3.6.2 大對象直接進入老年代

  • 大對象是指須要大量連續的內存空間的Java對象,最典型的大對象就是那種很長的字符串以及數組。
  • 虛擬機提供了一個參數:PretenureSizeThreshold,大於這個參數的對象將直接在老年代分配。

3.6.3 長期存活的對象將進入老年代

  • 虛擬機給每一個對象定義了一個對象年齡計數器(Age),對象每通過一次Minor GC後仍然存活,且能被Survivor容納的話,年齡就 +1 ,當年齡增長到必定程度(默認爲15),就會被晉升到老年代中,這個閾值能夠經過參數 MaxTenuringThreshold 來設置。

4.動態對象年齡的斷定

3.6.4 動態對象年齡斷定

  • 若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代。

3.6.5 空間分配擔保

  • 爲了更好的適應不一樣程序的內存情況,對象年齡不是必須到達閾值纔會進入老年代。
  • 只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,不然將進行Full GC。

問題

爲何程序要跑到安全點時停下來?

  • 不設置安全點,而讓每一條指令都產生Oop(Ordinary Object Pointer)會須要大量的額外空間,增大GC的空間成本。設置了合適的安全點,有助於虛擬機得知對象引用所在的地方,所以有利於GC對「即將回收」的對象進行掃描。

最後上一張本章結構圖

圖片來源於網絡若有侵權請私信刪除
圖片描述
轉至:https://segmentfault.com/a/1190000010421285
相關文章
相關標籤/搜索