你瞭解JVM垃圾回收機制及其實現原理嗎?一文帶你深入探討

前言

對於 JVM 來說,我們都不陌生,其實 Java Virtual Machine(Java 虛擬機)的縮寫,它也是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM 有自己完善的硬件架構,如處理器、堆棧等,還具有相應的指令系統,其本質上就是一個程序,當它在命令行上啓動的時候,就開始執行保存在某字節碼文件中的指令。

Java 語言的可移植性就是建立在 JVM 的基礎之上的,任何平臺只要裝有針對於該平臺的 Java 虛擬機,字節碼文件(.class)就可以在該平臺上運行,這就是“一此編譯,多次運行”。除此之外,作爲 Java 語言最重要的特性之一的自動垃圾回收機制,也是基於 JVM 實現的。那麼,自動垃圾回收機制到底是如何實現的呢?在本文中,就讓我們一探究竟。

什麼是垃圾?

在 JVM 進行垃圾回收之前,首先就是判斷哪些對象是垃圾,也就是說,要判斷哪些對象是可以被銷燬的,其佔有的空間是可以被回收的。根據 JVM 的架構劃分,我們知道, 在 Java 世界中,幾乎所有的對象實例都在堆中存放,所以垃圾回收也主要是針對堆來進行的。

在 JVM 的眼中,垃圾就是指那些在堆中存在的,已經“死亡”的對象。而對於“死亡”的定義,我們可以簡單的將其理解爲“不可能再被任何途徑使用的對象”。那怎樣才能確定一個對象是存活還是死亡呢?這就涉及到了垃圾判斷算法,其主要包括引用計數法和可達性分析法。

垃圾判斷算法

引用計數法

在這種算法中,假設堆中每個對象(不是引用)都有一個引用計數器。當一個對象被創建並且初始化賦值後,該對象的計數器的值就設置爲 1,每當有一個地方引用它時,計數器的值就加 1,例如將對象 b 賦值給對象 a,那麼 b 被引用,則將 b 引用對象的計數器累加 1。

反之,當引用失效時,例如一個對象的某個引用超過了生命週期(出作用域後)或者被設置爲一個新值時,則之前被引用的對象的計數器的值就減 1。而那些引用計數爲 0 的對象,就可以稱之爲垃圾,可以被收集。

特別地,當一個對象被當做垃圾收集時,它引用的任何對象的計數器的值都減 1。

  • 優點:引用計數法實現起來比較簡單,對程序不被長時間打斷的實時環境比較有利。
  • 缺點:需要額外的空間來存儲計數器,難以檢測出對象之間的循環引用。

可達性分析法

可達性分析法也被稱之爲根搜索法,可達性是指,如果一個對象會被至少一個在程序中的變量通過直接或間接的方式被其他可達的對象引用,則稱該對象就是可達的。更準確的說,一個對象只有滿足下述兩個條件之一,就會被判斷爲可達的:

  • 對象是屬於根集中的對象
  • 對象被一個可達的對象引用

在這裏,我們引出了一個專有名詞,即根集,其是指正在執行的 Java 程序可以訪問的引用變量(注意,不是對象)的集合,程序可以使用引用變量訪問對象的屬性和調用對象的方法。在 JVM 中,會將以下對象標記爲根集中的對象,具體包括:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 方法區中的常量引用的對象
  • 方法區中的類靜態屬性引用的對象
  • 本地方法棧中 JNI(Native 方法)的引用對象
  • 活躍線程(已啓動且未停止的 Java 線程)

根集中的對象稱之爲GC Roots,也就是根對象。可達性分析法的基本思路是:將一系列的根對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,如果一個對象到根對象沒有任何引用鏈相連,那麼這個對象就不是可達的,也稱之爲不可達對象。

當標記階段完成後,GC 開始進入下一階段,刪除不可達對象。當然,可達性分析法有優點也有缺點,

  • 優點:可以解決循環引用的問題,不需要佔用額外的空間
  • 缺點:多線程場景下,其他線程可能會更新已經訪問過的對象的引用

引用類型

  1. 強引用:發生 gc 的時候不會被回收
  2. 軟引用:有用但不是必須的對象,在發生內存溢出之前會被回收
  3. 弱引用:有用但不是必須的對象,在下一次 GC 時會被回收
  4. 虛引用(幽靈引用/幻影引用):無法通過虛引用獲得對象用 PhantomReference 實現虛引用,虛引用的用途是在 gc 時返回一個通知

垃圾辨別方法

  1. 引用計數器爲每個對象創建一個引用計數,有對象引用時計數器 +1,引用被釋放時計數 -1當計數器爲 0 時就可以被回收。缺點是不能解決循環引用的問題
  2. 可達性分析從 GC Roots 開始向下搜索,搜索所走過的路徑稱爲引用鏈當一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是可以被回收的

GC Roots,GC 的根集合, 是一組必須活躍的引用

可作爲 GC Roots 的對象有:

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

垃圾收集算法

  1. 引用計數(Reference Counting)原理是此對象有一個引用,即增加一個計數,刪除一個引用則減少一個計數垃圾回收時,只用收集計數爲 0 的對象缺點:無法處理循環引用問題
  2. 標記-清除(Mark-Sweep)第一階段從引用根節點開始標記所有被引用的對象第二階段遍歷整個堆,把未標記的對象清除缺點:此算法需要暫停整個應用,同時,會產生內存碎片
  3. 複製(Copying)把內存空間劃爲兩個相等的區域,每次只使用其中一個區域垃圾回收時,遍歷當前使用區域,把正在使用中的對象複製到另外一個區域中。此算法每次只處理正在使用中的對象因爲複製成本比較小,同時複製過去以後還能進行相應的內存整理,不會出現“碎片”問題缺點:需要兩倍內存空間
  4. 標記-整理(Mark-Compact)第一階段從引用根節點開始標記所有被引用對象第二階段遍歷整個堆,將所有存活的對象都向一端移動,然後直接清除掉端邊界以外的內存此算法避免了“標記-清除”的碎片問題,同時也避免了“複製”算法的空間問題
  5. 分代(Generational Collecting)基於對對象生命週期分析後得出的垃圾回收算法把堆中對象分爲年青代、年老化、持久代(JDK8 不存在持久代)對不同生命週期的對象使用不同的算法進行回收現在的垃圾回收器一般使用此算法

分代回收算法

起源:研究發現,大部分 java 對象只存活一小段時間,而存活下來的小部分 java 對象則會存活很長一段時間

簡單來說,將堆分成兩部分,年輕代用來存放新對象,當對象存活時間夠長時,移動到年老代

堆的分代

  • 年輕代 Young Generation
  1. 默認佔總空間的 1/3(通過 -XX:NewRatio 指定年輕代和老年代比例)
  2. 分爲 Eden、To Survivor、From Survivor 三個區,默認佔比 8:1:1(通過 -XX:SurvivorRatio 指定)
  • 年老代 Tenured Generation
  1. 默認佔總空間的 2/3
  • 持久代 Perm Generation(JDK8後不存在)
  1. 即方法區,用於存放靜態文件,如今Java類、方法等
  2. 持久代對垃圾回收沒有顯著影響在
  3. JDK8 中,廢棄了持久代,改用元空間(metaspace)實現方法區,屬於本地內存

分代收集

  • 年輕代回收器
  1. 假設大部分對象都存活很短時間,需要頻繁採用耗時較短的垃圾回收算法
  2. 新生代垃圾收集器一般採用複製算法,優點是效率高,缺點是內存利用率低
  3. 垃圾收集器有:Serial、ParNew、Parallel Scavenge
  • 年老代回收器
  1. 假設老年代中的對象大概率繼續存活,真正觸發老年代 gc 時,代表假設出錯或堆空間已耗盡,一般需要全堆掃描,全局垃圾回收
  2. 老年代收集器一般採用的是標記-整理的算法進行垃圾回收
  3. 垃圾收集器有:Serial Old、Parallel Old、CMS
  • 整堆回收器

G1:兼顧吞吐量和停頓時間的 GC 實現,JDK 9 以後的默認 GC 選項

回收過程

新對象存放在年輕代的 Eden 分區,Eden 空間耗盡時,觸發 gc,一般使用複製算法

年老代空間佔用到達某個值之後就會觸發全局垃圾收回,一般使用標記整理的執行算法

  1. 把 Eden 和 From Survivor 存活的對象放入 To Survivor 區
  2. 清空 Eden 和 From Survivor 分區
  3. From 和 To 交換指針,保證下次 gc 前To Survivor 爲空
  4. Survivor 分區的對象,經過一次複製年齡就 +1,年齡到達 15時(默認 15),Survivor 分區升級爲老生代。對象也會直接進入年老代

gc 類型

  • Minor GC
  1. 一般情況下,當新對象生成,並且在 Eden 申請空間失敗時,就會觸發Minor GC
  2. 在年輕代 Eden 區域進行GC,清除不存活對象,並且把尚且存活的對象移動到 Survivor 區。然後整理 Survivor 的兩個區
  3. 很頻繁的 gc,不影響老年代
  • Full GC

對整個堆進行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此應該儘可能減少Full GC。有如下原因可能導致Full GC:

  • Tenured 被寫滿
  • Perm 域被寫滿(JDK8 之前)
  • System.gc( ) 被顯示調用
  • 上一次 GC 之後對的各域分配策略動態變化

垃圾收集器

收集器分類

  • 串行收集器
  1. 使用單線程處理所有垃圾回收工作,因爲無需多線程交互,所以效率比較高
  2. 無法使用多處理器的優勢,所以適合單處理器機器,也可以用在小數據量情況下的多處理器機器
  3. 可以使用 -XX:+UseSerialGC 打開
  • 併發收集器
  1. 對年輕代進行並行垃圾回收,可以減少垃圾回收時間。一般在多線程多處理器機器上使用使用 -XX:+UseParallelGC打開
  2. 打開並行收集器 jdk5 引入,在 jdk6 中進行了增強,可對堆年老代進行並行收集使用 -XX:+UseParallelOldGC 打開
  3. 如果年老代不使用併發收集,而使用單線程進行垃圾回收,會制約擴展能力
  • 併發收集器
  1. 可以保證大部分工作都併發進行(應用不停止),垃圾回收只暫停很少的時間
  2. 此收集器適合對響應時間要求比較高的中、大規模應用
  3. 使用 -XX:+UseConcMarkSweepGC 打開

常見收集器

  1. Serial:最早的單線程串行垃圾回收器
  2. Serial Old:Serial 垃圾回收器的老年版本,同樣也是單線程的,可以作爲 CMS 垃圾回收器的備選預案
  3. ParNew:是 Serial 的多線程版本
  4. Parallel :和 ParNew 收集器類似,是多線程的收集器Parallel 是吞吐量優先的收集器,可以犧牲等待時間換取系統的吞吐量
  5. Parallel Old:是 Parallel 老生代版本Parallel 使用複製算法,Parallel Old 使用標記-整理算法
  6. CMS:一種以獲得最短停頓時間爲目標的收集器,非常適用 B/S 系統
  7. G1:一種兼顧吞吐量和停頓時間的 GC 實現,是 JDK 9 以後的默認 GC 選項

CMS 收集器

  • CMS:Concurrent Mark-Sweep
  1. 犧牲吞吐量來獲得最短回收停頓時間
  2. 非常適合用在要求服務器響應速度的應用上使用 -XX:+UseConcMarkSweepGC 來指定
  3. 使用 CMS 垃圾回收器
  • CMS 使用標記-清除的算法
  1. 在 gc 時候會產生大量的內存碎片
  2. 當剩餘內存不能滿足程序運行要求時,系統將會出現 Concurrent Mode Failure
  3. 臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低

喜歡請多多點贊評論轉發,關注小編,你們的支持就是小編最大的動力!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章