學習 JVM 發現挺有意思的,感覺都是知識乾貨,但是容易忘,為了準備秋招,本菜雞隻好把內容總結一遍,供各位看官點評點評,本文只涉及垃圾回收部分。
少俠莫慌,先上一張圖壓壓驚
why - 為什麼要了解垃圾回收
- 排查各種記憶體溢位、記憶體洩漏的問題
- 突破垃圾回收成為系統達到更高併發量的瓶頸
what1 - 哪些記憶體區域需要回收
- 不需要回收的區域:程式計數器、虛擬機器棧、本地方法棧
這 3 個區域是執行緒私有,每個棧幀分配記憶體基本上是在類結構確定下來時就已知,記憶體分配和回收具備確定性,並且方法或執行緒結束時,記憶體自然也就回收了,不需要考慮回收問題 - 需要回收的區域:堆、方法區
Java 堆和方法區的記憶體分配是動態的,只有在程式執行期間才知道會建立哪些物件,需要關注的是這兩部分記憶體回收
what2 - 哪些物件需要回收(物件存活判定演算法)
引用計數演算法
- 原理:給物件新增一個引用計數器,每當有一個地方引用它時,計數器就加 1;當引用失效時,計數器值就減 1;任何時刻計數器為 0 的物件就是不可能再被使用的
- 優點:實現簡單、判定效率高
- 缺點:存在物件之間迴圈引用的問題
可達性分析演算法(GC Roots)
- 原理:通過一系列的稱為「GC Roots」的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的
- GC Roots 物件種類
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中常量引用的物件
- 本地方法棧中 JNI(即一般說的 Native 方法)引用的物件
when - 什麼時候回收
Minor GC 的觸發條件
- Eden 區域沒有足夠的空間時,發起一次 Minor GC
Full GC 的觸發條件
- 呼叫 System.gc() 時,系統建議執行 Full GC,但是不必然執行
- 老年代空間不足時
- 方法區空間不足時
- 歷次通過 Minor GC 後進入老年代的平均大小大於老年代的可用記憶體時
- 由 Eden 區、From Survior 區向 To Survior 區複製時,物件大小大於 To Survior 區可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小時
Minor GC 和 Full GC 的區別
-
新生代GC(Minor GC)
指發生在新生代的垃圾收集動作,因為 Java 物件大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快 -
老年代GC(Major GC / Full GC)
指發生在老年代的 GC,出現了 Major GC,經常會伴隨至少一次的 Minor GC。 Major GC的速度一般會比 Minor GC 慢 10 倍以上。
how - 如何回收(垃圾收集演算法)
標記 - 清除演算法
- 該演算法分為「標記」和「清除」兩個階段,首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。
- 特點
- 效率不高:標記和清除兩個過程的效率都不高
- 空間問題:標記清楚之後會產生大量不連續的記憶體碎片,空間碎片太多會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發一次垃圾收集動作。
複製演算法(新生代採用的演算法)
- 將可用記憶體按容量劃分為大小相等的兩塊,每次使用其中的一塊,當這一塊的記憶體用完了,就將還存活的物件複製到另一塊上面,然後把已使用過的記憶體空間一次清理掉
- 特點
- 優點:每次對整個半區進行回收,記憶體分配時不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。
- 缺點:記憶體縮小為了原來的一半
標記 - 整理演算法
- 該演算法分為「標記」和「整理」兩個階段,其中「標記」階段與「標記-清除」演算法中的標記相同;在整理階段不是直接對可回收物件進行清理,而是讓所有存活的物件向一端移動,然後直接清理掉端邊界以外的記憶體
- 特點
- 優點:避免了空間碎片,空間利用率提高
- 缺點:效率不高,標記和清除過程的效率低下
分代收集演算法
- 將 Java 堆分為新生代和老年代,根據各個年代的特點採用適當的收集演算法。
- 在新生代中,每次垃圾收集時都發現只有少量存活,選擇使用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集
- 在老年代中,因為物件存活率高、沒有額外空間進行分配擔保,使用「標記-清理」或者「標記-整理」演算法進行回收
垃圾收集器
衡量指標
- JVM 吞吐量
所謂吞吐量就是 CPU 用於執行程式碼的時間與 CPU 總消耗時間的比值,即吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間) - 停頓時間
一段時間內 JVM 執行程式碼的執行緒讓渡給 GC執行緒執行垃圾回收而暫停使用者程式碼執行的時長,在這段時間內沒有應用程式是活動的 - 兩者的關係
- 理想的 JVM 垃圾收集器是“吞吐量越高越好,停頓時間越短越好” ;
- 無法同時兼顧高吞吐量和低停頓時間。在選擇 JVM 垃圾收集器時,我們必須確定我們切實可行的目標:一個 GC 演算法只可能專注於最大吞吐量或最小停頓時間,或者嘗試找到一個權衡兩者這種的方案;
- 停頓時間越短就越適合與使用者互動的程式,良好的響應速度能提升使用者體驗;而高吞吐量則可以高效率利用 CPU 時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務;
Serial 收集器
-
特點
- 單執行緒收集器,只會使用一個 CPU 或一條收集執行緒去完成垃圾收集工作;
- 垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束(又稱為「Stop The World」);
- 優點是簡單而高效(與其他收集器的單線層相比),對於限定單個 CPU 的環境來說,Serial 收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然就可以獲得最高的單執行緒收集效率;
-
應用場景及引數設定
- 場景:該收集器是 HotSpot 虛擬機器執行在 Client 模式下的預設的新生代收集器適合執行在單 CPU 環境
- 演算法:堆記憶體年輕代採用“複製演算法”;堆記憶體老年代採用“標記-整理演算法”
- 配置:+xx:UseSerialGC;年輕代採用Serial,老年代採用 Serial Old
ParNew 收集器
-
特點
- ParNew 收集器就是 Serial 收集器的多執行緒版本;
- 只能用於新生代;
- 多執行緒收集,並行;
- 在多 CPU 環境下,隨著 CPU 的數量增加,它對於 GC; 時系統資源的有效利用是有益的。它預設開啟的收集執行緒數與 CPU 的數量相同;
- ParNew 收集器在單 CPU 的環境中絕對不會有比 Serial 收集器有更好的效果,甚至由於存線上程互動的開銷,該收集器在通過超執行緒技術實現的兩個 CPU 的環境中都不能百分之百地保證可以超越;
-
應用場景及引數設定
- 演算法:堆記憶體年輕代採用“複製演算法”
- 配置:+xx:UseParNewGC;年輕代採用ParNew,老年代採用 Serial Old
-XX:ParallerGCThreads;多 CPU 情況下面開啟多少個執行緒來回收記憶體
Parallel Scavenge 收集器
-
特點
- 多執行緒收集器
- 只適用於新生代
- 自適應調節策略
- Parallel Scavenge收集器的目標是達到一個可控制的吞吐量
- Parallel Scavenge 收集器無法與 CMS 收集器配合使用
-
應用場景及引數設定
- 新生代:複製演算法。設定引數:-XX:+UseParallelGC;
- 老年代:使用多執行緒和“標記-整理”演算法。設定引數:-XX:+UseParallelOldGC;
- -XX:ParallelGCThreads=,並行 GC 執行緒數;
- -XX:MaxGCpauseMillis,設定最大垃圾收集停頓時間;
- -XX:GCTimeRatio,設定吞吐量大小;
- -XX:+UseAdaptiveSizePolicy,這是一個動態調整各個代區的記憶體大小的開關引數,開啟引數後,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例(-XX:SurvivorRatio)、晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種方式稱為 GC 自適應的調節策略(GC Ergonomics)
CMS 收集器
-
運作過程
- 初始標記
僅僅只是標記一下 GC Roots 能直接關聯到的物件,速度很快,需要“Stop The World” - 併發標記
進行 GC Roots 追溯所有物件的過程,在整個過程中耗時最長 - 重新標記
為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。此階段也需要“Stop The World” - 併發清除
- 初始標記
-
特點
- Concurrent Mark Sweep,基於“標記-清除”演算法實現
- 各階段耗時:併發標記/併發清除 > 重新標記 > 初始標記
- 對 CPU 資源非常敏感
- 標記-清除演算法導致的空間碎片
- 併發收集、低停頓,因此 CMS 收集器也被稱為併發低停頓收集器
- 無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次 Full GC 的產生
- 由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作;所以,從總體上來說,CMS 收集器的記憶體回收過程是與使用者執行緒一起併發執行的。
-
應用場景及引數設定
- 當你的應用程式需要有較短的應用程式暫停,而可以接受垃圾收集器與應用程式共享應用程式時,你可以選擇 CMS 垃圾收集器
- -XX:+UseConcMarkSweepGC,使用 CMS 收集器
- -XX:+UseCMSCompactAtFullCollection,Full GC 後,進行一次碎片整理,整理過程是獨佔的,會引起停頓時間變長
- -XX:+CMSFullGCsBeforeCompaction,設定進行幾次 Full GC 後,進行一次碎片整理
- -XX:ParallelCMSThreads,設定 CMS 的執行緒數量(一般情況約等於可用 CPU 數量)
G1 收集器
-
運作過程
- 初始標記
僅僅只是標記一下 GC Roots 能直接關聯到的物件,速度很快,需要“Stop The World” - 併發標記
進行 GC Roots 追溯所有物件的過程,可與使用者程式併發執行 - 最終標記
修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄 - 篩選回收
對各個Region的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來指定回收計劃
- 初始標記
-
特點
- 面向服務端應用的垃圾收集器
- 並行與併發
- 分代收集
- 空間整合:整體上看來是基於“標記-整理”演算法實現的,從區域性(兩個Region)上看來是基於“複製”演算法實現的
- 可預測的停頓:G1收集器可以非常精確地控制停頓,既能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java的垃圾收集器的特徵
- G1將整個Java堆(包括新生代、老年代)劃分為多個大小相等的記憶體塊(Region),每個 Region 是邏輯連續的一段記憶體,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收垃圾最多的區域
-
應用場景及引數設定
- -XX:MaxGCPauseMillis = 50 設定最大允許 GC 時間
內容補充
分代收集演算法的原因
- 為什麼JVM堆記憶體新生代選用“複製演算法”?
- 在新生代中,由於大量的物件都是“朝生夕死”,也就是一次垃圾收集後只有少量物件存活,因此HotSpot JVM將堆記憶體劃分成三塊:Eden、Survior1、Survior2,記憶體大小分別是8:1:1。
- 分配記憶體時,只使用 Eden 和一塊 Survior。例如,當發現 Eden+Survior1 的記憶體即將滿時,JVM會發起一次MinorGC,清除掉廢棄的物件,並將所有存活下來的物件複製到另一塊Survior2 中。那麼,接下來就用 Eden+Survior2 進行記憶體分配。通過這種方式,只需要浪費 10% 的記憶體空間即可實現帶有壓縮功能的垃圾收集方法,避免了記憶體碎片的問題。
- 但是,當一個物件要申請記憶體空間時,發現 Eden+Survior 中剩下的空間無法放置該物件,此時需要進行Minor GC,如果 Minor GC 過後空閒出來的記憶體空間仍然無法放置該物件,那麼此時就需要將可用物件轉移到老年代中,然後再將新物件存入 Eden 區,這種方式叫做“分配擔保”。
- 為什麼JVM堆記憶體老年代選用“標記-整理演算法”?
- 老年代中的物件一般壽命比較長,因此每次GC時會有大量物件存活,因此如果選用“複製”演算法,每次需要複製大量存活的物件,會導致效率很低。而且,在新生代中使用“複製”演算法,當Eden+Survior中都裝不下某個物件時,可以使用老年代的記憶體進行“分配擔保”,而如果在老年代使用該演算法,那麼在老年代中如果出現裝不下某個物件時,沒有其他區域給他作分配擔保。因此,老年代中一般使用“標記-整理”演算法。
物件自我拯救
- 第一次標記:物件進行可達性分析後發現沒有與 GC Roots 相連線的引用鏈,將進行第一次標記並進行一次篩選,判斷物件是否覆蓋了 finalize() 方法
- 若已覆蓋該方法,並且該物件的 finalize() 方法還沒有被執行過,那麼就將改物件扔到 F-Queue 佇列中
- 若沒有覆蓋 finalize() 方法或該物件已經執行過該方法,則進入「即將回收」的集合
- 第二次標記:虛擬機器自動建立的、低優先順序的 Finalizer 執行緒去執行 F-Queue 佇列,實際上是去執行佇列中物件的 finalize() 方法,GC會對 F-Queue 佇列中的物件進行第二次小規模的標記,如果該物件重新與引用鏈上的任何一個物件建立關聯 (比如把自己 ( this關鍵字 ) 賦值給某個類變數或物件的成員變數),第二次標記會將它移出「即將回收」的集合
引用的分類
- 強引用(Strong Reference)
我們平時所使用的引用就是強引用。 A a = new A(); 也就是通過關鍵字new建立的物件所關聯的引用就是強引用。 只要強引用存在,該物件永遠也不會被回收。 - 軟引用(Soft Reference)
只有當堆即將發生OOM異常時,JVM才會回收軟引用所指向的物件。 軟引用通過SoftReference類實現。 軟引用的生命週期比強引用短一些。 - 弱引用(Weak Reference)
只要垃圾收集器執行,軟引用所指向的物件就會被回收。 弱引用通過WeakReference類實現。 弱引用的生命週期比軟引用短。 - 虛引用(Phantom Reference)
虛引用也叫幽靈引用,它和沒有引用沒有區別,無法通過虛引用取得一個物件例項。 一個物件關聯虛引用唯一的作用就是在該物件被垃圾收集器回收之前會受到一條系統通知。 虛引用通過PhantomReference類來實現。
本文為筆者整理的讀書筆記,如有錯誤的地方麻煩指出,歡迎各位大佬指導。
筆者自己的公眾號,待秋招之後開始寫文章,歡迎關注。
複製程式碼
參考來源:《深入理解 Java 虛擬機器》、公眾號:Java 大後端