JVM系列深入理解JVM垃圾回收

NO IMAGE

前言

在 Java 虛擬機中,一個 Java 對象被加載進 JVM 後,它的生命週期被劃分為 7 個階段:

JVM系列深入理解JVM垃圾回收

如上圖,對象的生命週期的 7 個階段分別為:創建階段、應用階段、不可見階段、不可達階段、收集階段、終結階段、以及對象內存空間重新分配階段。

  • 創建階段

創建階段的步驟主要可以分為:

(1)為對象分配空間;
(2)構造對象;
(3)從超類到子類對 static 成員進行初始化;
(4)遞歸調用超類的構造方法;
(5)調用子類的構造方法;

  • 應用階段

當應用被初始化賦初值後,就切換進入應用階段。這一階段的對象至少具有一個強引用、或者顯式地使用軟引用、弱引用、或者虛引用;

  • 不可見階段

在應用程序中找不到對象的任何強引用,例如程序的執行已經超出了對象的作用域。但此時的對象仍然有可能被特殊的 GC Roots 所持有,例如對象被本地方法棧中的 JNI 引用或者被運行中的線程引用等;

  • 不可達階段

對象不被任何強引用所引用,並且垃圾收集器發現不可達;

  • 收集階段

垃圾收集器已經發現該對象不可達,並且垃圾收集器準備對該對象的內存空間重新分配。如果這時候垃圾收集器發現該對象重寫了 finalize() 方法,垃圾收集器會豁免該對象的收集,並且調用 finalize() 方法。如果該對象沒有重寫 finalize() 方法,則等待垃圾收集器回收該對象的內存空間。

  • 終結階段

此時對象可能執行了 finalize() 方法(GC 不一定會等待該對象的 finalize() 方法執行完),或者該對象沒有重寫 finalize() 方法,這時候等待垃圾收集器收集該對象的內存空間。

  • 對象空間重新分配階段

當對象被 GC 回收了內存空間,該對象的生命週期就完全結束了。

以上,是一個對象被加載進 JVM 中的生命週期。而在 Java 虛擬機中,對象的回收對程序員是不可見的,也就是說一旦對象不被其他對象所引用,就有可能被 GC 標記為不可達,進而等待 GC 的回收。在 Java 虛擬機回收不被引用的對象的時候,會經歷對象的標記、以及對象被垃圾收集器的回收過程。

垃圾標記算法

在 Java 虛擬機中,垃圾對象(當一個對象不被其他對象所持有的時候被稱為垃圾對象)的標記算法,可以分為 引用計數法可達性分析 (也有部分文章把可達性分析稱為根搜索算法) 。

引用計數法

在《深入理解 Java 虛擬機》一書中,給出的引用計數法的定義:給對象中添加一個引用計數器,每當有一個地方引用它時,引用計數器的值就會加 1;當引用失效的時候,計數器值就減 1;任何時刻計數器為 0 的對象就是不可能再被使用的。但在目前主流的商用虛擬機中都沒有采用引用計數法,原因是它很難解決對象之間互相引用的問題,如下代碼:

JVM系列深入理解JVM垃圾回收

JVM系列深入理解JVM垃圾回收

如上代碼,當執行

TestReferenceCountingGC gc_1 = new TestReferenceCountingGC();
TestReferenceCountingGC gc_2 = new TestReferenceCountingGC();
gc_1.instance = gc_2;
gc_2.instance = gc_1;

的時候,由於 new TestReferenceCountingGC()new TestReferenceCountingGC() 兩個對象被引用了兩次,如果根據引用計數算法,那麼 new TestReferenceCountingGC()new TestReferenceCountingGC() 的引用計數器的值都為 2。當執行 gc_1 = null;gc_2 = null; 的時候,就會有 1 次引用失效,那麼 new TestReferenceCountingGC()new TestReferenceCountingGC() 還有 1 次引用,那麼如果 Java 虛擬機採用的是引用計數算法標記垃圾對象,這兩個對象的內存空間不會被垃圾收集器所回收,應該會出現如下 GC 日誌:

[GC (System.gc()) [PSYoungGen: 9339K->4872K(76288K)] 9339K->4880K(251392K), 0.0057164 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

但是在實際中,卻出現瞭如下的 GC 日誌:

[GC (System.gc()) [PSYoungGen: 9339K->776K(76288K)] 9339K->784K(251392K), 0.0015327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

上述的 GC 日誌證明目前的 Java 虛擬機的垃圾標記算法,並不是採用引用計數算法。

可達性分析

可達性分析的主要思路就是通過一系列的稱為 GC Roots 的對象作為起點,然後從這個節點往下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain)。當一個對象到 GC Roots 沒有任何的引用鏈相聯(在圖論中,就是從 GC Roots 到這個對象不可達)時,則證明這個對象時不用用的。

JVM系列深入理解JVM垃圾回收

在 Java 中,可以作為 GC Root 的有以下幾種(部分):

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  • 方法區中的靜態屬性引用的對象;
  • 方法區中的 final 關鍵字修飾的常量引用的對象;
  • 本地方法棧中 JNI 引用的對象;

在 JDK 1.2 以前,引用的定義:在虛擬機棧的局部變量表中 reference 類型的數據中存儲的數值代表的是另一種內存的起始地址,就稱為這塊內存代表著一個引用。但這種定義的說法只能用來定義被引用、和沒有被引用這兩種狀態。為了可以描述這樣的一類對象:當內存足夠的時候,則保留在內存之中,如果內存空間在進行垃圾收集後,內存佔用還是非常緊張,則可以回收這些對象。

於是,提出了強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phanton Reference):

  • 強引用類似於 Object obj = new Object() 這類的引用,只要強引用存在,垃圾收集器永遠不會回收這類對象;

  • 軟引用是一種相對於強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認為內存不足時,才會回收軟引用指向的對象。JVM 會確保在拋出 OOM 之前,清理軟引用指向的對象;

  • 弱引用並不能豁免垃圾收集,僅僅時提供訪問在弱引用狀態下對象的途徑。被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前的內存是否足夠,都會回收掉只被弱引用關聯的對象;

  • 虛引用也被稱為幽靈引用或者幻影引用,你不能通過它訪問對象。虛引用僅僅時提供了一種確保對象在 finalize() 以後,做某些事情的機制,如能在對象被垃圾收集器回收時收到一個系統的通知。

垃圾收集算法

標記-清除算法

標記-清除算法分為兩個階段:

  • 標記階段:標記可以被回收的對象;
  • 清除階段:回收被標記的對象內存;

標記-清除算法時最基礎的算法,因為後面提到的垃圾回收算法都是基於此算法的基礎上面改造的,標記-清除算法的執行過程如下:

JVM系列深入理解JVM垃圾回收

標記-清除算法主要有兩個缺點:一是標記和清除的效率都不高;二是如上圖所示,在標記清除可回收的對象空間後,會產生大量不連續的內存碎片,碎片太多可能會導致後續沒有足夠的內存分配給較大的對象,從而導致觸發新一輪的垃圾收集動作。

複製算法

為了解決標記-清除算法帶來的內存碎片的問題,於是提出了複製算法。複製算法把內存空間劃分為大小相等的兩塊,每次只使用其中的一塊,然後再把另一塊內存空間清理掉:

JVM系列深入理解JVM垃圾回收

複製算法存在著複製效率低的不足,並且如果不想浪費 50% 空間內存,則需要提供額外的空間擔保,以應對被使用的內存中所有的對象都 100% 存活的極端情況。

標記-整理算法

複製算法一般不使用在老年代,因為在老年代中,大部分的對象的存活率比較高,選擇複製算法就會導致過多的複製操作,導致效率變低。同時也不採用標記-清除算法,因為會產生過多的內存碎片,導致容易觸發新的一輪垃圾回收動作。於是出現了一種標記-整理算法(標記-壓縮算法)。標記-整理算法與標記-清除算法不同的是,在標記完內存中的對象以後,把存活下來的對象壓縮到內存的一端,使得他們緊湊地排序在一起,然後對存活對象邊界外的對象進行回收。

JVM系列深入理解JVM垃圾回收

分代收集

分代收集算法會結合不同的多種垃圾算法來處理不同的空間,因此在學習分代收集算法之前首先需要了解 Java 堆的空間劃分。Java 堆被劃分為新生代(Young Generation)和老年代(Tenured Generation),而新生代又被細分為 Eden 空間、From Survivor 空間和 To Survivor 空間。因為在 Java 堆裡面,大部分對象都是”朝生夕滅”,只有少數的對象的生命週期比較長,甚至有的對象的生命週期和虛擬機的生命週期一樣長,對不同對象地生命週期採用不同的垃圾收集算法,這就是分代收集的概念。

根據 Java 堆的空間的劃分,垃圾收集最要可以分為兩種方法:

  • Minor GC: 新生代垃圾收集;
  • Full GC: 又稱為 Major GC,Full GC 通常至少會伴隨一次 Minor GC,它的收集頻率較低,耗時較長。

當執行一次 Minor GC 的時候,虛擬機會把 Eden 空間中存活的對象複製到 To Survivor 空間,同時把 From Survivor 空間存活的對象也複製到 To Survivor 空間,然後再把 Eden 空間和 From Survivor 空間裡面的所有對象清除,這時候把 To Survivor 空間的指針指向 From Survivor 空間,也就是說 To Survivor 空間的名字變成了 From Survivor 空間,以等待下一次 Minor GC 的來臨。當然,並不是所有的新對象都是分配在 Eden 空間的,當新對象需要佔用的內存空間要比 Eden 空間可用的空間要大的時候,新對象會直接分配在老年代。

當對象在新生代經過一定數量的 Minor GC 後仍然存活,那麼虛擬機會把該對象晉升到老年代中。虛擬機給每個對象都定義了一個對象年齡(Age)計數器。當新對象在 Eden 空間經過一次 Minor GC 仍然存活,並且可以被 Survivor 空間接納,就把對象的年齡計數器設為 1,然後該對象每經過一次 Minor Gc,就把該對象的年齡計數器加 1,當對象的年齡計數器達到晉升老年代的閥值的時候,該對象就會晉升到老年代中,一般虛擬機設為 15。

當然,虛擬機也不一定需要對象的年齡計數器的值達到了晉升老年代的閥值來晉升對象的。如果在 Survivor 空間中相同年齡所有對象大小的總和大於 Survivor 空間的一半,年齡大於或等於該年齡的對象就可以進入老年代,而無需等到對象的年齡計數器的值滿足晉升老年代的閥值。

JVM系列深入理解JVM垃圾回收

小結

原本還打算把內存的分配和回收策略、GC 日誌分析也寫在這裡的,但瞄了瞄本章的篇幅,感覺有點篇幅過長了,哈哈哈…那….我就不寫啦。

相關文章

Flutter開發踩坑記錄(乾貨總結)

聊聊畢業設計系列系統實現