1. 程式人生 > 其它 >《深入理解Java虛擬機器》(三)垃圾收集器與記憶體分配策略

《深入理解Java虛擬機器》(三)垃圾收集器與記憶體分配策略

垃圾收集器與記憶體分配策略 詳解

3.1 概述

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

3.2 物件已死嗎

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

3.2.1 引用計數演算法

在物件中新增一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器減1;其中計數器為0的物件是不可能再被使用的已死物件。

  • 當兩個物件相互引用時,這兩個物件就不會被回收
  • 引用計數演算法,不被主流虛擬機器採用,主要原因是它很難解決物件之間相互迴圈引用的問題。

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)

演算法分為兩個階段:標記和清除

標記:首先標記所有需要回收的物件 清除:在標記完成後統一回收所有被標記的物件

標記過程在上文宣告一個物件死亡過程中提及

缺點

  • 效率問題,標記和清除兩個過程的效率都不高(回收後空間碎片過多,再次回收(即可達性分析時)有時需要遍歷整個記憶體區域)。
  • 空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體,而不得不提前觸發另一次垃圾收集動作。

圖片來源於網路如有侵權請私信刪除

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對“即將回收”的物件進行掃描。

最後上一張本章結構圖

圖片來源於網路如有侵權請私信刪除

《深入理解Java虛擬機器:JVM高階特性與最佳實踐_周志明.高清掃描版.pdf》

下載地址:連結:http://pan.baidu.com/s/1miBQCBY 密碼:9kbn

推薦閱讀

《深入理解Java虛擬機器》(一)Java虛擬機發展史

《深入理解Java虛擬機器》(二)Java虛擬機器執行時資料區

《深入理解Java虛擬機器》(三)垃圾收集器與記憶體分配策略

《深入理解Java虛擬機器》(四)虛擬機器效能監控與故障處理工具

《深入理解Java虛擬機器》(五)JVM調優 - 工具

《深入理解Java虛擬機器》(六)堆記憶體使用分析,GC 日誌解讀

Contact

  • 作者:鵬磊
  • 出處:http://www.ymq.io
  • Email:[email protected]
  • 版權歸作者所有,轉載請註明出處
  • Wechat:關注公眾號,搜雲庫,專注於開發技術的研究與知識分享