JVM基礎系列第8講:JVM 垃圾回收機制


在第 6 講中我們說到 Java 虛擬機的內存結構,提到了這部分的規范其實是由《Java 虛擬機規范》指定的,每個 Java 虛擬機可能都有不同的實現。其實涉及到 Java 虛擬機的內存,就不得不談到 Java 虛擬機的垃圾回收機制。因為內存總是有限的,我們需要一個機制來不斷地回收廢棄的內存,從而實現內存的循環利用,這樣程序才能正常地運轉下去。

比起 Java 虛擬機的內存結構有《Java 虛擬機規范》規定,垃圾回收機制並沒有具體的規范約束。所以很多時候不同的虛擬機有不同的實現方式,下面所說的垃圾回收都是以 HotSpot 虛擬機為例。

到底誰是垃圾?

要進行垃圾回收,最為重要的一個問題是:判斷誰是垃圾?

聯想其日常生活中,如果一個東西經常沒被使用,那么這個對象可以說就是垃圾。在 Java 中也是如此,如果一個對象不可能再被引用,那么這個對象就是垃圾,應該被回收。

根據這個思想,我們很容易想到使用引用計數的方法來判斷垃圾。在一個對象被引用時加一,被去除引用時減一,這樣我們就可以通過判斷引用計數是否為零來判斷一個對象是否為垃圾。這種方法我們一般稱之為「引用計數法」。

上面的這種方法雖然簡單,但是其存在一個致命的問題,那就是循環引用。

A 引用了 B,B 引用了 C,C 引用了 A,它們各自的引用計數都為 1。但是它們三個對象卻從未被其他對象引用,只有它們自身互相引用。從垃圾的判斷思想來看,它們三個確實是不被其他對象引用的,但是此時它們的引用計數卻不為零。這就是引用計數法存在的循環引用問題。

而現今的 Java 虛擬機判斷垃圾對象使用的是:GC Root Tracing 算法。其大概的過程是這樣:從 GC Root 出發,所有可達的對象都是存活的對象,而所有不可達的對象都是垃圾。

可以看到這里最重要的就是 GC Root 這個集合了,其實 GC Root 就是一組活躍引用的集合。但是這個集合又與一般的對象集合不太一樣,這些集合是經過特意篩選出來的,通常包括:

  • 所有當前被加載的 Java 類
  • Java 類的引用類型靜態變量
  • Java類的運行時常量池里的引用類型常量
  • VM的一些靜態數據結構里指向GC堆里的對象的引用
  • 等等

簡單地說,GC Root 就是經過精心挑選的一組活躍引用,這些引用是肯定存活的。那么通過這些引用延伸到的對象,自然也是存活的。

如何進行垃圾回收?

到這里,我們了解了什么是垃圾以及 JVM 是如何判斷垃圾對象的。那么識別出垃圾對象之后,JVM 是如何進行垃圾回收的呢?這就是我們下面要講的內容:如何進行垃圾回收?

垃圾回收算法簡單地說有三種算法:標記清除算法、復制算法、標記壓縮算法。

標記清除算法。從名字可以看到其分為兩個階段:標記階段和清除階段。一種可行的實現方式是,在標記階段,標記所有由 GC Root 觸發的可達對象。此時,所有未被標記的對象就是垃圾對象。之后在清除階段,清除所有未被標記的對象。標記清除算法最大的問題就是空間碎片問題。如果空間碎片過多,則會導致內存空間的不連續。雖說大對象也可以分配在不連續的空間中,但是效率要低於連續的內存空間。

復制算法。復制算法的核心思想是將原有的內存空間分為兩塊,每次只使用一塊,在垃圾回收時,將正在使用的內存中的存活對象復制到未使用的內存塊中。之后清除正在使用的內存塊中的所有對象,之后交換兩個內存塊的角色,完成垃圾回收。該算法的缺點是要將內存空間折半,極大地浪費了內存空間。

標記壓縮算法。標記壓縮算法可以說是標記清除算法的優化版,其同樣需要經歷兩個階段,分別是:標記結算、壓縮階段。在標記階段,從 GC Root 引用集合觸發去標記所有對象。在壓縮階段,其則是將所有存活的對象壓縮在內存的一邊,之后清理邊界外的所有空間。

對比一下這三種算法,可以發現他們都有各自的優點和缺點。

標記清除算法雖然會產生內存碎片,但是不需要移動太多對象,比較適合在存活對象比較多的情況。而復制算法雖然需要將內存空間折半,並且需要移動存活對象,但是其清理后不會有空間碎片,比較適合存活對象比較少的情況。而標記壓縮算法,則是標記清除算法的優化版,減少了空間碎片。

分代思想

試想一下,如果我們單獨采用任何一種算法,那么最終的垃圾回收效率都不會很好。其實 JVM 虛擬機的建造者們也是這么想的,因此在實際的垃圾回收算法中采用了分代算法。

所謂分代算法,就是根據 JVM 內存的不同內存區域,采用不同的垃圾回收算法。例如對於存活對象少的新生代區域,比較適合采用復制算法。這樣只需要復制少量對象,便可完成垃圾回收,並且還不會有內存碎片。而對於老年代這種存活對象多的區域,比較適合采用標記壓縮算法或標記清除算法,這樣不需要移動太多的內存對象。

試想一下,如果沒有采用分代算法,而在老年代中使用復制算法。在極端情況下,老年代對象的存活率可以達到100%,那么我們就需要復制這么多個對象到另外一個內存區域,這個工作量是非常龐大的。

在這里我們再深入地聊一聊新生代里采取的垃圾回收算法。如我們上面所說,新生代的特點是存活對象少,適合采用復制算法。而復制算法的一種最簡單實現便是折半內存使用,另一半備用。但實際上我們知道,在實際的 JVM 新生代划分中,卻不是采用等分為兩塊內存的形式。而是分為:Eden 區域、from 區域、to 區域 這三個區域。那么為什么 JVM 最終要采用這種形式,而不用 50% 等分為兩個內存塊的方式?

要解答這個問題,我們就需要先深入了解新生代對象的特點。根據IBM公司的研究表明,在新生代中的對象 98% 是朝生夕死的,所以並不需要按照1:1的比例來划分內存空間。所以在HotSpot虛擬機中,JVM 將內存划分為一塊較大的Eden空間和兩塊較小的Survivor空間,其大小占比是8:1:1。當回收時,將Eden和Survivor中還存活的對象一次性復制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Eden空間。

通過這種方式,內存的空間利用率達到了90%,只有10%的空間是浪費掉了。而如果通過均分為兩塊內存,則其內存利用率只有 50%,兩者利用率相差了將近一倍。

分區思想

分代思想按照對象的生命周期長短將其分為了兩個部分(新生代、老年代),但 JVM 中其實還有一個分區思想,即將整個堆空間划分成連續的不同小區間。

每一個小區間都獨立使用,獨立回收,這種算法的好處是可以控制一次回收多少個區間,可以較好地控制 GC 時間。

到這里我們基本上把 JVM 的垃圾回收都將清除了,從一開始什么是垃圾,到之后如何判斷垃圾,到如何回收垃圾,到垃圾回收的兩個重要思想:分代思想、分區思想。通過這么一個脈絡,我們了解了垃圾回收的整體概括。在下面的章節中,我們將深入介紹這其中的細節。

參考資料


如果只是看,其實無法真正學會知識的。為了幫助大家更好地學習,我建了一個虛擬機群,專門討論學習 Java 虛擬機方面的內容,每周針對我所發文章進行討論答疑。如果你有興趣,關注「Java技術精選」公眾號,通過右下角菜單「入群交流」加我好友,小助手會拉你入群。


JVM基礎系列目錄


免責聲明!

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



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