《深入理解Java虛擬機器》學習筆記之垃圾收集器與記憶體分配策略
一、概述
-
GC(Garbage Collection)需要完成的三件事
- (1)哪些記憶體需要回收
- (2)什麼時候回收
- (3)如何回收
-
GC主要面向Java堆和方法區中的記憶體
- 原因:這部份記憶體的分配和回收都是動態的
- 只有在程式處於執行期間時才能知道會建立哪些物件
- 程式計數器、虛擬機器棧、本地方法棧三個區域隨執行緒而生、隨執行緒而滅,記憶體分配和回收具有確定性
- 原因:這部份記憶體的分配和回收都是動態的
二、物件已死嗎(判斷物件是否存活)
1、引用計數演算法
-
基本思想
- 給物件中新增一個引用計數器
- 每當一個地方引用它時,計數器的值就+1
- 當引用失效時,計數器的值就-1
- 任何時刻計數器為0的物件就是不可能再被使用的
-
問題:難以解決物件間迴圈引用的問題
- 只存在相互引用時,兩個物件的引用計數器均不為0,但也會被回收
- 因此虛擬機器並未採用引用計數演算法來判定物件是否存活
2、可達分析演算法(主流實現,Java/C#等均使用)
-
基本思想
- 通過一系列稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋
- 搜尋所走過的路徑稱為引用鏈(Reference Chain)
- 當一個物件到GC Roots沒有任何引用鏈時,則證明此物件可回收
-
可作為GC Roots的物件
- 虛擬機器棧(棧幀中的本地變量表)中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中常量引用的物件
- 本地方法棧中JNI(即一般說的Native方法)引用的物件
3、再談引用(四種引用型別,強>軟>弱>虛)
-
(1)強引用
-
定義:類似
Object obj = new Object()
這樣的引用 -
回收時機:只要強引用存在,垃圾收集器永遠不會回收掉被引用的物件(永遠不回收)
-
-
(2)軟引用
-
定義:有用但並非必需的物件引用
-
實現:
SoftReference
-
回收時機:在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍中進行第二次回收(記憶體不足時回收)
- 如果這次回收還沒有足夠記憶體,才會丟擲記憶體溢位異常
-
-
(3)弱引用
-
定義:非必需物件的引用
-
實現:
WeakReference
-
回收時機:當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件(只要GC就會回收)
- 被弱引用關聯的物件只能生存到下一次垃圾收集完成之前。
-
-
(4)虛引用
-
定義:無法通過一個虛引用來獲取一個物件例項
- 一個物件是否有虛引用例項對其生存時間無影響
-
實現:
PhantomReference
-
存在意義:在被虛引用關聯的物件被GC回收時能收到一個系統通知
- 宣告虛引用的時候是要傳入一個queue的。當虛引用所引用的物件已經執行完finalize函式的時候,就會把物件加到queue裡面。可以通過判斷queue裡面是不是有物件來判斷你的物件是不是要被回收了。
-
4、生存還是死亡(物件死亡的兩次標記過程)
-
第一次標記(同時進行一次是否需要執行
finalize()
方法的篩選)-
標記標準:如果物件在可達性分析後發現沒有與GC Roots相連線的引用鏈
-
篩選標準:
- (1)當物件沒有覆蓋
finalize()
方法 - (2)當物件的
finalize()
方法已經被虛擬機器呼叫過- 任何一個物件的
finalize()
方法都只會被系統自動的呼叫一次
- 任何一個物件的
- (1)當物件沒有覆蓋
-
篩選結果:如果滿足兩種篩選條件中的任意一種,均不必要執行
finalize()
方法
-
-
第二次標記
-
前提條件:這個物件被判定有必要執行
finalize()
方法 -
過程:
- 物件在第二次標記期間將會被放到一個
F-Queue
的隊列當中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer
執行緒去執行它。(“執行”指只觸發執行緒但不等待,避免執行緒阻塞導致其他佇列中的物件無法回收) - 然後GC將對
F-Queue
中的物件進行第二次小規模的標記。 - 兩次標記後進行回收
- 物件在第二次標記期間將會被放到一個
-
物件的自救:
-
方法:重寫
finalize()
方法,重新與引用鏈上的物件建立連結- 例如將自己(this)賦值給某個變數或物件的成員變數
-
結果:在第二次標記時,將被移出
F-Queue
-
注:儘量不要用這種方法,而是選擇
try/catch
-
-
5、回收方法區
-
方法區(HotSpot虛擬機器中的永久代)相較於堆中的新生代垃圾收集效率底很多
-
永久代垃圾回收內容:
-
廢棄常量
- 滿足判定標準時,發生GC時會被回收
-
無用的類
-
類無用的時候也不一定會被回收,是否對類回收,可以通過HotSpot虛擬機器中提供的
-Xnoclassgc
引數進行控制,還可以使用-verbose:class
以及-XX:+TraceClassLoading
、-XX:+TraceClassLoading
檢視類載入和解除安裝資訊 -
頻繁自定義ClassLoader的場景需要虛擬機器具備類解除安裝的功能,以保證永久代不會溢位
-
-
-
(1)廢棄常量判定標準
- 沒有常量物件/類(介面)/方法/欄位的符號引用常量池中的常量
- 也沒有其他地方引用該常量的字面量
-
(2)無用的類判定標準(同時滿足三個條件)
-
條件一:該類所有的例項都已經被回收
- 即Java堆中不存在任何該類的例項
-
條件二:載入該類的ClassLoader已經被回收
-
條件三:該類對應的java.lang.Class物件沒有在人和地方被引用,無法在任何地方通過反射訪問該類的方法
-
三、垃圾收集演算法(四種)
1、標記-清除演算法
-
思想:
-
標記出所有需要回收的物件
- 標記:指判定物件是否已死時的兩次標記
-
在標記完成後統一回收所有被標記的物件
-
-
不足;
-
標記和清除過程效率都較低
-
標記清除後會產生大量不連續的記憶體碎片問題
- 空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠連續的記憶體而不得不提前觸發另一次垃圾收集動作
-
2、複製演算法(一般應用於新生代)
-
思想:
- 將可用的記憶體按容量劃分為大小相等的兩塊,每次只使用其中一塊
- 當一塊的記憶體用完了,就將還存活著的物件按順序複製到另一塊上面,然後堆已使用的半區進行一次性清除
-
優點:
- 不用考慮記憶體碎片
- 移動後在另一個半區是按順序排列好的,一次回收完整半區
- 不用考慮記憶體碎片
-
不足:
- 實際可用記憶體被縮小
- 在物件存活率較高時就要進行較多的複製操作,效率將會變低
-
商業實現(一般用於回收新生代)
-
前提;研究表明新生代中的物件98%是“朝生夕死”的
-
思想:
-
將記憶體分為一塊較大的Eden空間(80%)和兩塊較小的Survivor空間(10% + 10%)
-
每次使用Eden和其中一塊Survivor
-
當回收時,將Eden和Survivor中還存活著的物件一次性的複製到另外一塊Survivor空間上,最後清理掉的之前使用的空間
-
這樣只有一塊Survivor的空間會被浪費
-
-
分配擔保
- 如果另外一塊Survivor空間沒有足夠容量存放上一次新生代收集下來的存活物件時,這些物件將直接通過分配擔保機制進入老年代
-
3、標記-整理演算法(一般應用於老年代)
-
原因:老年代中物件存活率較高,複製演算法成本過高
-
思想:
-
標記過程與“標記-清除”演算法一致
-
標記後讓所有存活的物件向一端移動(整理)
-
然後直接清理掉邊界以外的記憶體
-
4、分代收集演算法(通常商業應用)
- 思想:
-
根據物件的存活週期的不同將記憶體劃分為幾塊
- 一般將Java堆分為新生代和老年代
-
(1)對於新生代:採用複製演算法
-
(2)對於老年代:標記-清除演算法/標記-整理演算法
-
四、HotSpot的演算法實現(如何發起記憶體回收)
1、列舉根結點(如何快速尋找物件引用)
-
根結點:以可達性分析的GC Roots根結點節點尋找引用鏈為例
- 問題引出:如何避免逐個檢查引用而消耗時間?
-
可達性分析對時間的敏感體現:GC停頓
- 可達性分析必須在一個能確保一致性的快照中進行
- “一致性”的快照:在分析過程期間,整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不斷變化的情況。否則分析結果的準確性就無法得到保證
- 可達性分析必須在一個能確保一致性的快照中進行
-
準確式GC
-
定義:虛擬機器可以知道記憶體中某個位置的資料具體是什麼型別(reference型別指向的地址or具體值),從而在GC的時候能夠準確判斷堆上的資料是否還可能被使用
-
好處:可以拋棄Classic VM基於handle(控制代碼)的物件查詢方式
-
因為在沒有明確資訊表明記憶體中哪些資料是reference的前提下,虛擬機器無法保證GC後物件存在因位置移動而出現的問題,所以要使用handle來保持reference的穩定
-
(1) Exact VM用直接指標而不是handle來實現Java層的引用
-
(2)通常,通過直接指標來訪問物件意味著“一次間接”,而通過控制代碼則意味著“兩次或更多次間接”
-
-
-
HotSpot通過一組被稱為
OopMap
的資料結構來達到直接得知哪些地方存放的是物件引用的目的-
在類載入完成時,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來
-
在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用
- JIT編譯是一種提高程式執行效率的方法
- JIT編譯器:在Java程式語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的位元組碼(包括需要被解釋的指令的程式)轉換成可以直接傳送給處理器的指令的程式。當你寫好一個Java程式後,源語言的語句將由Java編譯器編譯成位元組碼,而不是編譯成與某個特定的處理器硬體平臺對應的指令程式碼(比如,Intel的Pentium微處理器或IBM的System/390處理器)。位元組碼是可以傳送給任何平臺並且能在那個平臺上執行的獨立於平臺的程式碼。
-
2、安全點
-
問題引出:避免引用關係變化(OopMap內容變化的指令)過多而產生的空間成本開銷
- 為每一條指令都生成對應的OopMap將消耗大量空間
-
解決方案:安全點
-
定義:程式執行時,並非在所有地方都能停頓下來開始GC,只有在到達“安全點”這樣的特定位置才能暫停
-
選取標準:以程式(指令集)“具有讓程式長時間執行的特徵”為標準
- 每條指令執行的時間很短,需要有足夠的時間給安全點
- 即使是指令流長度很長也無法保證程式長時間執行
- “長時間執行”的明顯特徵:指令序列複用。如方法呼叫、迴圈跳轉、異常跳轉等
-
-
如何在GC發生時,讓所有執行緒(執行JNI呼叫執行緒除外)都到達最近的安全點上停頓下來?
-
(1)搶先式中斷(幾乎不使用)
- 在GC發生時,首先將所有執行緒全部中斷
- 如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它到達安全點
- 不需要執行緒的執行程式碼主動配合
-
(2)主動式中斷
- 當GV需要中斷執行緒時,不直接對執行緒操作,僅僅簡單的設定一個標誌,各個執行緒執行時主動區輪詢這個標誌
- 發現中斷標誌為真時就自己中斷掛起
- 輪詢標誌的地方和安全點是重合的
-
3、安全區域(擴大的安全點)
-
問題引出:線上程不執行(沒有分配CPU時間,如處於Sleep狀態或Block狀態時)時,無法響應JVM的中斷請求,到達安全的地方去中斷掛起,JVM也不可能等待執行緒被重新分陪CPU時間
-
解決方案:安全區域
-
定義:在安全區域(一段程式碼)中,引用關係不會發生變化,在這個區域中的任意地方開始GC都是安全的
-
思想:
- 當執行緒執行到Safe Region中的程式碼時,首先標識自己已經進入了Safe Region(在擁有標識的期間內,該執行緒在GC時可以不用再檢查)
- 當執行緒要離開Safe Region時,要檢查系統是否已經完成了根結點列舉(或整個GC過程),如果完成了,則繼續執行,否則就必須等待直到收到離開Safe Region的標識
-
五、垃圾收集器(7種)
- 上圖是7種作用於不同分代的收集器,如果兩個收集器之間存在連線,說明它們可以搭配使用
1、Serial收集器
-
概述:
- 是最基本、發展歷史最悠久的收集器
- 是虛擬機器執行在Client模式下的預設新生代收集器
-
特點:
-
主要用於收集新生代
-
採用分代收集演算法
-
是一個單執行緒的收集器
- 它的“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作
- 更重要的是它在進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束
-
-
優點:簡單而高效
- 限定單個CPU環境,沒有執行緒互動開銷
2、PerNew收集器
-
概述:
- 是執行在Server模式下的虛擬機器中首選的新生代收集器
- 原因:除了Serial收集器外,目前只有他能與CMS收集器配合工作
- 是執行在Server模式下的虛擬機器中首選的新生代收集器
-
特點:
-
主要用於收集新生代
-
採用分代收集演算法
-
多執行緒收集器
-
實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作
- HotSpot虛擬機器中第一款真正意義上的併發(Concurrent)收集器
-
預設開啟的GC執行緒數與CPU的數量相同
-
5、Parallel Scavenge收集器
- 特點:
-
用於收集新生代
-
使用複製演算法
-
目標:“吞吐量優先”收集器
-
吞吐量:CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值(吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間))
-
可理解為吞吐量與允許最大GC時間成正比
-
Parallel Scavenge收集器的目標是達到一個可控制的吞吐量(CMS等收集器的關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間)
-
-
適合在後臺運算而不需要太多互動的任務
-
停頓時間越短就越利於使用者互動,良好的響應速度能提升使用者體驗
-
高吞吐量可以高效率的利用CPU時間
-
-
並行的多執行緒收集器
- 並行與併發:並行是指同一時刻同時做多件事情,而併發是指同一時間間隔內做多件事情(併發通過時間片排程實現現實意義的並行)
-
4、Serial Old收集器
-
概述:
-
是Serial收集器的老年代版本
-
主要意義也是在於給Client模式下虛擬機器使用
-
如果在Server模式下,它主要還有兩大用途
- (1)與Parallel Scavenge收集器搭配使用
- (2)作為CMS收集器的後備預案,在併發收集發生Conurrent Mode Failure使用
-
-
特點:
-
用於收集老年代
-
使用標記-整理收集演算法
-
5、Parallel Old收集器
-
概述:
- Parallel Old是Parallel Scavenge收集器的老年代版本
-
特點:
-
用於收集老年代
-
使用“標記-整理”演算法
-
多執行緒
-
-
應用場景:在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old收集器
6、CMS(Concurrent Mark Sweep)收集器
-
特點:
-
用於收集老年代
-
基於“標記-清除”演算法實現
-
目標:儘可能地縮短垃圾收集時使用者執行緒的停頓時間
-
-
標記-清除過程:
-
①初始標記(速度快,但需確保"Stop the World")
-
②併發標記(耗時長,但可與使用者執行緒一起工作)
- 併發標記階段就是進行GC Roots Tracing的過程
-
③重新標記(速度快,但需確保"Stop the World")
- 目的:為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄
-
④併發清除(耗時長,但可與使用者執行緒一起工作)
-
-
缺點(三條):
-
(1)CMS收集器對CPU資源非常敏感
- 併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒(或者說CPU資源)而導致應用程式變慢,總吞吐量會降低。
-
(2)CMS收集器無法處理浮動垃圾,可能出現“Conurrent Mode Failure”失敗而導致另一次Full GC的產生
-
由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會產生新的垃圾,這一部分垃圾出現在標記過程之後,CMS無法在檔次收集中處理掉它們,只好留待下一次GC時再清理掉。這部分垃圾就稱為“浮動垃圾”。
-
此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時程式運作使用。在JDK1.5的預設設定下,CMS收集器當老年代使用了68%的空間後就會被啟用。如果預留空間無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案Serial Old收集器
-
-
(3)CMS是一款基於“標記-清除”演算法實現的收集器,會有大量空間碎片問題。
-
5、G1收集器
-
概述:
- 是當今收集器技術發展的最前沿成果之一
- 是一款面向服務端應用的垃圾收集器
-
特點:
-
採用分代收集(既能回收新生代,也能回收老年代)
- 可以不需要其他收集器的配合管理整個堆,但是仍採用不同的方式去處理分代的物件。
-
並行與併發
- 能充分利用多CPU,多核環境下的硬體優勢,縮短Stop-The-World停頓的時間,同時可以通過併發的方式讓Java程式繼續執行
-
空間整合
- G1從整體上來看,採用基於“標記-整理”演算法實現收集器
- G1從區域性上來看,採用基於“複製”演算法實現
-
可預測停頓
-
使用G1收集器時,Java堆記憶體佈局與其他收集器有很大差別,它將整個Java堆劃分成為多個大小相等的獨立區域。
-
G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表
-
每次根據允許的收集時間(根據使用者建立的可控時間模型),優先回收價值最大的Region
-
-
-
運作過程:
-
初始標記(速度快,但需確保"Stop the World")
-
併發標記(耗時長,但可與使用者執行緒一起工作)
-
最終標記(需停頓執行緒,但是可並行執行)
- ???
-
篩選回收(時間可控,可與使用者執行緒一起工作)
-
六、記憶體分配與回收策略
-
自動記憶體管理解決的兩個問題
- (1)給物件分配記憶體
- (2)回收分配給物件的記憶體
-
物件的記憶體分配大致規律(主要是堆上分配)
-
主要分配在新生代的Eden區上
- 如果啟動了本地執行緒分配緩衝(TLAB),將按執行緒優先在TLAB上分配
-
少數情況下也可能直接分配在老年代上
-
-
Minor GC和Full GC的區別
-
Minor GC
:指發生在新生代的垃圾收集動作,該動作非常頻繁,速度也快。 -
Full GC/Major GC
:指發生在老年代的垃圾收集動作,出現了Major GC,經常會伴隨至少一次的Minor GC。Major GC的速度一般會比Minor GC慢10倍以上。
-
1、物件優先在Eden分配
-
大多數情況下,物件在新生代的Eden區中分配
-
當Eden區沒有足夠空間進行分配時,虛擬機器將發起一次
Minor GC
2、大物件直接進入老年代
- 大物件:需要大量連續記憶體空間的Java物件
- eg:很長的字串及陣列
3、長期存活的物件將進入老年代
-
虛擬機器給每個物件定義了一個物件年齡(Age)計數器
-
年齡增長規則:
-
如果物件在Eden出生並經過第一次Minor GC後依然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且物件年齡設定為1
-
物件進入Survivor空間之後,就只會伴隨每次GC在兩個Survivor空間來回複製
-
物件在Survivor區中每“熬過”一次Minor GC,年齡+1
-
直到年齡達到閾值,進入老年代
- 預設閾值為15歲
- 物件今生老年代的年齡閾值可以通過
-XX:MaxTenuringThreshold
引數設定
-
4、動態物件年齡判定
-
虛擬機器並不是永遠的要求物件的年齡必須達到了
MaxTenuringThreshold
才能晉升老年代 -
如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無需等到
MaxTenuringThreshold
5、空間分配擔保
-
目的:避免出現存活物件在GC時空間不足以容納的情況
-
過程(兩步檢查):
-
第一步檢查: 在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間
-
(1)如果這個條件成立,那麼Minor GC可以 確保是安全的。
-
(2)如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗
-
-
第二步檢查:如果允許擔保失敗,那會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小
-
如果大於,則將嘗試進行一次Minor GC,儘管這個Minor GC是有風險的
-
如果小於,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。
-
-
-
分析:
-
第一步檢查:
-
所冒“風險”:新生代使用複製收集演算法,只使用其中一個Survivor空間來作為輪換備份,當Survivor空間不足以存放存活物件時,需要使用老年代來進行分配擔保,讓Survivor無法容納的物件直接進入老年代
-
因此:老年代最大可用的連續空間大於新生代所有物件的總空間能確保空間足夠
-
-
第二步檢查:
-
取過去的平均值來推測可能需要的物件空間進行比較是一種動態概率手段,存在風險(如某次Minor GC存活後的物件突增)
-
解決:如果出現HandlePromotionFailure失敗,那麼只好在失敗後重新發起一次Full GC
-
好處:大部分時間還是會將HandlePromotionFilure設定為允許,避免Full GC過於頻繁(Full GC成本很高)
-
-