☕[JVM技術指南](1)垃圾回收子系統(Garbage Collection System)之回收標記和物件引用的介紹

liboware發表於2021-06-11

什麼是GC垃圾回收(Garbage Collection),為什麼需要GC回收(Garbage Collection)?

垃圾(Garbage)是指在執行程式中沒有任何指標指向的物件,這個物件就是需要被回收的垃圾。

為什麼需要GC?

  1. 如果不及時的對垃圾進行清理,那麼這些垃圾物件所佔用的物件,一直會保留到應用程式結束,並且垃圾物件所佔用的這部分空間不能被其它物件所使用,這樣如果程式中的物件比較多的時候,可能會導致記憶體溢位,並且這樣也是比較浪費空間的
  2. Java程式的記憶體,不需要且不支援開發者手動回收(例如,C++的解構函式等功能),全部交由JVM進行管理,降低程式程式設計的門檻!

回收記憶體的型別

手動回收

在早期的C/C++時代,垃圾回收基本上是手工進行的。開發人員可以使用new關鍵字進行記憶體申請,並使用delete關鍵字進行記憶體釋放。

存在的問題

  • 這種方式可以靈活控制記憶體釋放的事件,但是會給開發人員帶來頻繁申請和釋放記憶體的管理負擔。

  • 倘若有一處記憶體區間由於程式設計師編碼的問題忘記被回收,那麼就會產生記憶體洩漏,垃圾物件永遠無法被清除,隨著系統執行時間的不斷增長,垃圾物件所耗記憶體可能持續上升,直到出現記憶體溢位並造成應用程式崩潰。

自動回收

自動記憶體管理,無需開發人員手動參與記憶體的分配與回收,這樣降低記憶體洩漏和記憶體溢位的風險。

  • 自動記憶體管理機制,將程式設計師從繁重的記憶體管理中釋放出來,可以更專心地專注於業務開發。

  • 沒有垃圾回收器,java也會和cpp一樣,各種懸垂指標,野指標,洩漏問題讓你頭疼不已。

存在的問題

  • Java開發人員而言,自動記憶體管理就像是一個黑匣子,如果過度依賴於”自動”,那麼這將會是一場災難,最嚴重的就會弱化Java開發人員在程式出現記憶體溢位時定位問題和解決問題的能力。

學習的原因

  • 瞭解JVM的自動記憶體分配和記憶體回收原理就顯得非常重要,只有在真正瞭解JVM是如何管理記憶體後,我們才能夠在遇見OutOfMemoryError時,快速地根據錯誤異常日誌定位問題和解決問題

  • 當需要排查各種記憶體溢位,記憶體洩漏問題時,當垃圾收整合為系統達到更高併發量的瓶頸時,我們就必須對這些”自動化”的技術實施必要的監控和調節。

  • 垃圾回收器可以對年輕代回收,也可以對老年代回收,甚至是全堆和方法區的回收,其中Java堆是垃圾收集器的工作重點。

從次數上講:

  • 頻繁收集Young區
  • 較少收集Tenure區
  • 基本不動Perm區(或元空間)

現在,除了Java以外,C#,Python,Ruby等語言都使用了自動垃圾回收的思想,也是未來發展趨勢。可以說,這種自動化的記憶體分配和垃圾回收的方式已經成為現代開發語言必備的標準。

如何定位垃圾?

  1. 在GC執行垃圾回收之前,首先需要區分出記憶體中哪些是存活物件,哪些是已經死亡的物件。只有標記那些物件是存活的,GC回收才會回收不會存活的物件(垃圾),釋放掉所佔用的記憶體空間。

  2. 那麼在JVM中究竟是如何標記一個死亡物件呢?當一個物件已經不再被任何的存活物件繼續引用時,就可以宣判為已經死亡

如何判斷對像是否存活,一般有兩種演算法,分別是引用計數法和可達性分析演算法,而引用計數法是很老的一種演算法,先簡單介紹這兩種演算法。

引用計數演算法

主流的Java虛擬機器裡面都沒有選用引用計數演算法來管理記憶體,主要原因是單純的引用計數很難解決物件之間相互迴圈引用的問題;

基本思路

在物件中新增一個引用計數器,每當有一個地方引用它時,計數器就 +1;當引用失效時,計數器值就 -1;任何時刻計數器為 0 的物件就是不可能再被使用的。

例如

objA.instance = objB 及objB.instance = objA,導致objA與objB互相引用著對方,導致被引用計數都不為 0,引用計數演算法就無法回收它們。

引用計數演算法的優缺點

優點

實現簡單,垃圾物件便於辨識;判定效率高,回收沒有延遲性

缺點

  1. 它需要單獨的欄位儲存計數器,這樣的做法增加了儲存空間的開銷
  2. 每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷。
  3. 引用計數器有一個嚴重的問題,即無法處理迴圈引用的情況。這是一條致命缺陷。導致在Java的垃圾回收器中沒有使用這類演算法。什麼叫做無法處理迴圈引用呢?如下圖:

☕[JVM技術指南](1)GC垃圾回收子系統介紹和分析
記憶體中的物件不再使用,但是這段記憶體的空間又釋放不掉,這中情況叫做記憶體洩漏。

可達性分析演算法

當前主流的商用程式語言(Java、C#)的記憶體管理子系統,都是通過可達性分析演算法來判定物件是否存活

  • 每次都會從根部進行掃描,如果發現一個物件沒有直接或間接的和根部相連,那麼這個物件就會被標記為死亡。

  • 在GC垃圾回收的時候,就會把這個物件回收掉。只有和根部直接或間接相連的物件才是存活的物件。

可達性分析演算法有叫做根搜尋演算法或追蹤性垃圾收集演算法

  1. 相對於引用計數演算法而言,可達性分析演算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該演算法可以有效地解決在引用計數演算法中迴圈引用的問題,防止記憶體洩漏的發生。

基本思路:

  1. 可達性分析演算法是以根物件集合(GC Roots)為起始點,按照從上至下的方式搜尋被根物件集合所連線的目標物件是否可達。

  2. 使用可達性分析演算法後,記憶體中的存活物件都會被跟物件集合直接或間接連線著,搜尋所走過的路徑稱為引用鏈(Reference Chain)

  3. 如果目標物件沒有任何引用鏈相連,則是不可達的,就意味著該物件已經死亡,可以屬於為垃圾物件

  4. 在可達性分析演算法中,只有能夠被跟物件集合直接或者間接連線的物件才是存活物件。

所謂GC Roots跟集合就是一組必須活躍的引用。

☕[JVM技術指南](1)GC垃圾回收子系統介紹和分析

GC Roots

在Java語言中,GC Roots包括以下幾類元素:

  1. 虛擬機器棧中引用物件,比如:各個執行緒被呼叫的方法中使用到的引數,區域性變數等

  2. 本地方法棧內JNI(通常說的本地方法)引用的物件

  3. 方法區中類靜態屬性引用的物件,比如:Java類的引用型別靜態變數

  4. 方法區中常量引用的物件,比如:字串常量池(String Table)裡的引用

  5. 所有被同步鎖synchronized持有的物件

  6. Java虛擬機器內部的引用,基本資料型別對應的Class物件,一些常駐的異常物件(如:NullPointerException,OutOfMemoryError),系統類載入器。

  7. 反映java虛擬機器內部情況的JMXBean,JVMTI中註冊的回撥,原生程式碼快取等

回收的過程

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程。

  1. 如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。

  2. 當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。程式中可以通過覆蓋finalize()來一場”驚心動魄”的自我拯救過程,但是,這隻有一次機會呦。

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”,這時候它們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記過程。

  • 第一次標記:如果物件在進行可達性分析後發現與GC Roots相連線的引用鏈,那它將會被第一次標記;

  • 第二次標記:第一次標記後會對所有需要回收物件進行一次篩選,篩選的條件是此物件是否有重寫finalize()方法,將所有這類待回收且覆蓋finalize()方法進行存放到Finalize-Queue中,之後將被進行第二次標記篩選過濾。

    • 第二次篩選後仍在GC-Queue佇列的物件將真的會被回收,如果物件在finalize()方法中重新與引用鏈建立了關聯關係,那麼會從GC佇列中移除,將會逃離本次回收,繼續存活。
引用介紹

GC垃圾回收器回收物件時,物件的有效性分析不僅僅是需要考慮物件可達性,還需要考慮物件的引用強度,從而使程式可以更加靈活地控制物件的生命週期。

可以用一個公式概括:物件的有效性=可達性+引用型別。

Java的設計人員把物件的引用細分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)四種級別,細分的準則是體現在被GC回收的優先順序上:強引用<軟引用<弱引用<虛引用。

  1. 強引用(Strong Reference)

    強引用表示一個物件處在【有用,必須】的狀態,是使用最普遍的引用。如果一個物件具有強引用,那麼垃圾回收器絕不會回收它。就算在記憶體空間不足的情況下,Java虛擬機器寧可丟擲OutOfMemoryError錯誤,使程式異常終止,也不會通過回收具有強引用的物件來解決記憶體不足的問題。

  2. 軟引用(Soft Reference)

軟引用表示一個物件處在【有用,但非必須】的狀態。在記憶體空間足夠的情況下,如果一個物件只具有軟引用,那麼垃圾回收器就不會回收它,但是如果記憶體空間不足,垃圾回收器就會回收這個物件(回收發生在OutOfMemoryError錯誤之前)。只要垃圾回收器沒有回收它,這個物件就能被程式使用。

  1. 弱引用(Weak Reference)

弱引用表示一個物件處在【可能有用,但非必須】的狀態。類似於軟引用,但是強度比軟引用更弱一些:只具有弱引用的物件擁有更短暫的生命週期。GC執行緒在掃描它所管轄的記憶體區域的過程中,一旦發現只具有弱引用的物件,就會回收掉這些被弱引用關聯的物件。也就是說,無論當前記憶體是否緊缺,GC都會回收被弱引用關聯的物件。不過,由於GC是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件

  1. 虛引用(Phantom Reference)

    虛引用表示一個物件處在【無用】的狀態,這意味著虛引用等同於沒有引用,在任何時候都可能被GC回收。設定虛引用的目的是為了被虛引用關聯的物件在被垃圾回收器回收的時候,能夠收到一個系統通知(用來跟蹤物件被GC回收的活動)。虛引用和弱引用的區別在於:虛引用的使用必須和引用佇列(Reference Queue)聯合使用

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章