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

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

比起 Java 虛擬機的內存結構有《Java 虛擬機規範》規定,垃圾回收機制並無具體的規範約束。因此不少時候不一樣的虛擬機有不一樣的實現方式,下面所說的垃圾回收都是以 HotSpot 虛擬機爲例。java

到底誰是垃圾?

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

聯想其平常生活中,若是一個東西常常沒被使用,那麼這個對象能夠說就是垃圾。在 Java 中也是如此,若是一個對象不可能再被引用,那麼這個對象就是垃圾,應該被回收。數據結構

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

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

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

而現今的 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基礎系列目錄

相關文章
相關標籤/搜索