Java的垃圾回收機制及算法


寫在前面:

該系列文章,主要是為了深入學習Java完成的一條鏈,推薦閱讀的整體順序為:Java的內存模型(根源),一個java文件被執行的歷程一個Java類的加載Java的垃圾回收機制及算法Linux(六):系統運維常用命令   Java程序運行狀態的監控(實用,定位Java程序問題)

Java的垃圾回收機制

前面的已經說過關於 Java在內存中的內存划分(Java的內存模型),一個java文件執行的流程、一個Java類如何被加載,本着有始有終的原則,下面該說一下 如何 “ 死亡 ”。

Java的編譯流程已經提過了,其實更多的就是類+對象+內存,就是將程序員抽象的類文件,加載到內存區,形成一個個的對象,對象可以極端的理解為就是內存中的一部分,內存划分好塊之后,對象都在堆中創建,其中一個對象,便是堆區中的一塊內存。

隨着程序的運行,各種套娃的邏輯,new的使用,數據的轉存等等,都會讓堆內的對象遞增,這就涉及一個問題,堆內的內存是有限的,對象如何只是不停的增加,早晚會爆炸,也就是常說的堆棧溢出;最經典就是C++,習慣寫C++的小伙伴肯定知道,每次創建對象,都要想着析構,因為C++是不會自己清除無效對象的,它的內存空間就會不停的增加,只有研發人員自發的去釋放掉,這樣就要求寫C++的研發要時刻牢記內存的釋放。

但是Java就不需要,我們只需要無腦的new new new ,套娃套娃套娃,原因就是Java有一套自動的對象銷毀機制,也叫垃圾回收機制,這也是學習JVM,以及各種面試都會問到的點。

通常要聊Java的垃圾回收機制,需要搞清除三個問題:

  1. 哪些內存會被回收清理
  2. 怎樣回收清理
  3. 什么時候會被回收清理

哪些內存(對象)會被回收清理

之前說過,在java中,萬物皆對象,而對象存在於堆內存中,要說哪些對象會被清除,其實可以理解為,哪些對象已經“死了”。也就是說哪些對象已經不再被程序需要,不再被調用,這里判斷對象是否需要被回收,一般是兩種算法:引用計數器算法和可達性算法

引用計數器法

原理其實很簡單,給運行的對象添加一個引用計數器,每當有一個地方引用它時,計數器+1;當引用失效時,計數器就-1,任何時刻計數器為0的對象,就視作不可能再被使用。這一種方式,實現簡單,邏輯也清晰,大部分的情況下,它都可以達到很好的效果,盡管這樣,計數器算法還是存在但是的,但是它無法解決循環引用的場景,這也是主流Java虛擬機沒有選用這一算法的原因。

可達性分析法

此算法的核心思想:通過一系列稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索走過的路徑稱為“引用鏈”,當一個對象到 GC Roots 沒有任何的引用鏈相連時(從 GC Roots 到這個對象不可達)時,證明此對象不可用。

想要了解什么是可達性對象GC Roots,需要先了解一下java的內存模型,java的內存模型

 

總體來說一般存在於:虛擬機棧、java方法區、本地方法區的對象都是可達的,也就是GCRoots對象

1、方法區靜態屬性引用的對象 全局對象的一種,Class對象本身很難被回收,回收的條件非常苛刻,只要Class對象不被回收,靜態成員就不能被回收。

2、方法區常量池引用的對象 也屬於全局對象,例如字符串常量池,常量本身初始化后不會再改變,因此作為GC Roots也是合理的。

3、方法棧中棧幀本地變量表引用的對象 屬於執行上下文中的對象,線程在執行方法時,會將方法打包成一個棧幀入棧執行,方法里用到的局部變量會存放到棧幀的本地變量表中。只要方法還在運行,還沒出棧,就意味這本地變量表的對象還會被訪問,GC就不應該回收,所以這一類對象也可作為GC Roots。

4、JNI本地方法棧中引用的對象 和上一條本質相同,無非是一個是Java方法棧中的變量引用,一個是native方法(C、C++)方法棧中的變量引用。

5、被同步鎖持有的對象 被synchronized鎖住的對象也是絕對不能回收的,當前有線程持有對象鎖呢,GC如果回收了對象,鎖不就失效了嘛。

可達性分析就是JVM首先枚舉根節點,找到一些為了保證程序能正常運行所必須要存活的對象,然后以這些對象為根,根據引用關系開始向下搜尋,存在直接或間接引用鏈的對象就存活,不存在引用鏈的對象就回收。注:注意的是被判定為不可達的對象不一定就會成為可回收對象。被判定為不可達的對象要成為可回收對象必須至少經歷兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收對象的可能性,則基本上就真的成為可回收對象了。

怎樣回收清理

使用發現算法垃圾被標記后,接下來就是如何去清除,常見的清除算法有:標記清除法、復制清除、標記整理算法、分代收集;其中當前主流使用的是分代收集

標記清除算法(Mark Sweep)

該算法很簡單,使用通過可達性分析分析方法標記出垃圾,然后直接回收掉垃圾區域。簡單粗暴,即標記刪除的對象,對其進行內存回收;它的一個顯著問題是一段時間后,內存會出現大量碎片,導致雖然碎片總和很大,但無法滿足一個大對象的內存申請,從而導致 OOM,而過多的內存碎片(需要類似鏈表的數據結構維護),也會導致標記和清除的操作成本高,效率低下。

 

標記整理算法(Generation Collection)

標記整理法,知識在標記清除的基礎上,追加了碎片的散落問題,在清除之后進行了碎片的整理,但副作用是增了了GC的時間。

復制算法(Copying)

為了解決標記清除算法的效率問題,有人提出了復制算法。它將可用內存一分為二,每次只用一塊,當這一塊內存不夠用時,便觸發 GC,將當前存活對象復制(Copy)到另一塊上,以此往復。這種算法高效的原因在於分配內存時只需要將指針后移,不需要維護鏈表等。但它最大的問題是對內存的浪費,使用率只有 50%。

但這種算法在一種情況下會很高效:Java 對象的存活時間極短。據 IBM 研究,Java 對象高達 98% 是朝生夕死的,這也意味着每次 GC 可以回收大部分的內存,需要復制的數據量也很小,這樣它的執行效率就會很高。

 

分代收集(Generation Collection)

在實際的Java程序中,大部分的對象存活周期都較短,基本上創建完,緊接着處理完數據就被丟棄了,大部分 Java 對象是朝生夕死的,所以我們將內存按照 Java 生存時間分為 新生代(Young)老年代(Old),前者存放短命僧,后者存放長壽佛,當然長壽佛也是由短命僧升級上來的。然后針對兩者可以采用不同的回收算法,比如對於新生代采用復制算法會比較高效,而對老年代可以采用標記-清除或者標記-整理算法。這種算法也是最常用的。

JVM Heap 分代后的划分一般如下所示,新生代一般會分為 Eden、Survivor0、Survivor1區,便於使用復制算法。

將內存分代后的 GC 過程一般類似下圖所示:

    1. 對象一般都是先在 Eden區創建

    2. Eden區滿,觸發 Young GC,此時將 Eden中還存活的對象復制到 S0中,並清空 Eden區后繼續為新的對象分配內存

    3. Eden區再次滿后,觸發又一次的 Young GC,此時會將 EdenS0中存活的對象復制到 S1中,然后清空EdenS0后繼續為新的對象分配內存

    4. 每經過一次 Young GC,存活下來的對象都會將自己存活次數加1,當達到一定次數后,會隨着一次 Young GC 晉升到 Old

    5. Old區也會在合適的時機進行自己的 GC

垃圾收集器運行機制

有了垃圾回收算法,那么其具體的實現就是垃圾收集器,也是我們實際使用中會具體用到的。現代的垃圾收集機制基本都是分代收集算法;新生代和老生代也有不同的回收器。

YoungOld區有不同的垃圾收集器,實際使用時會搭配使用,也就是上圖中兩兩連線的收集器是可以搭配使用的。這些垃圾收集器按照運行原理大概可以分為如下幾類:

  • Serial GC,串行,單線程的收集器,運行 GC 時需要停止所有的用戶線程,且只有一個 GC 線程

  • Parallel GC,並行,多線程的收集器,是 Serial 的多線程版,運行時也需要停止所有用戶線程,但同時運行多個 GC 線程,所以效率高一些

  • Concurrent GC,並發,多線程收集器,GC 分多階段執行,部分階段允許用戶線程與 GC 線程同時運行,這也就是並發的意思。

什么時候會被回收清理

  1. Young 區的GC 都是在 Eden 區滿時觸發

  2. Serial Old 和 Parallel Old 在 Old 區是在 Young GC 時預測Old 區是否可以為 young 區 promote 到 old 區 的 object 分配空間,如果不可用則觸發 Old GC。這個也可以理解為是 Old區滿時。

  3. CMS GC 是在 Old 區大小超過一定比例后觸發,而不是 Old 區滿。這個原因在於 CMS GC 是並發的算法,也就是說在 GC 線程收集垃圾的時候,用戶線程也在運行,因此需要預留一些 Heap 空間給用戶線程使用,防止由於無法分配空間而導致 Full GC 發生。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM