JVM垃圾收集機制(四)

1 JVM內存各區域回收分析

 

Java虛擬機的內存模型分爲五個部分,分別是:程序計數器、Java虛擬機棧、本地方法棧、堆、方法區。

 

程序計數器Java虛擬機棧本地方法棧都是線程私有的,也就是每條線程都擁有這三塊區域,而且會隨着線程的創建而創建,線程的結束而銷燬。

 

方法區中存放類信息、靜態成員變量、常量。類的加載是在程序運行過程中,當需要創建這個類的對象時纔會加載這個類。因此,JVM究竟要加載多少個類也需要在程序運行期間確定。

中存放JVM運行期間的所有對象,雖然每個對象的內存大小在加載該對象所屬類的時候就確定了,但究竟創建多少個對象只有在程序運行期間才能確定。

因此,堆和方法區便是JVM垃圾回收的重點。

 

2.堆內存的回收

 

2. 1 如何判定哪些對象需要回收?

 

在對堆進行對象回收之前,首先要判斷哪些是無效對象。我們知道,一個對象不被任何對象或變量引用,那麼就是無效對象,需要被回收。一般有兩種判別方式:

 

  • 引用計數法

每個對象都有一個計數器,當這個對象被一個變量或另一個對象引用一次,該計數器加一;若該引用失效則計數器減一。當計數器爲0時,就認爲該對象是無效對象。

 

  • 可達性分析法

所有和GC Roots直接或間接關聯的對象都是有效對象,和GCRoots沒有關聯的對象就是無效對象。

通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些結點開始向下搜索,搜索走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。如下圖,5,6,7雖然相互有關聯,但是它們到GCRoots是不可達的,所以會被判定是可回收對象。


 

GC Roots是指:

 

  1. Java虛擬機棧所引用的對象(棧幀中局部變量表中引用類型的變量所引用的對象)
  2. 方法區中靜態屬性引用的對象
  3. 方法區中常量所引用的對象
  4. 本地方法棧所引用的對象

PS:注意!GC Roots並不包括堆中對象所引用的對象!這樣就不會出現循環引用。

 

兩者對比:

引用計數法雖然簡單,但存在一個嚴重的問題,它無法解決循環引用的問題。

因此,目前主流語言均使用可達性分析方法來判斷對象是否有效。

 

2.2 回收無效對象的過程

 

JVM篩選出失效的對象之後,並不是立即清除,而是再給對象一次重生的機會,具體過程如下:

 

  1. 判斷該對象是否覆蓋了finalize()方法

若已覆蓋該方法,並且該對象的finalize()方法還沒有被執行過,那麼就會將finalize()扔到F-Queue隊列中;

若未覆蓋該方法,則直接釋放對象內存。

 

  1. 執行F-Queue隊列中的finalize()方法

虛擬機會以較低的優先級執行這些finalize()方法們,也不會確保所有的finalize()方法都會執行結束。如果finalize()方法中出現耗時操作,虛擬機就直接停止執行,將該對象清除。

 

  1. 對象重生或死亡

如果在執行finalize()方法時,將this賦給了某一個引用,那麼該對象就重生了。如果沒有,那麼就會被垃圾收集器清除。

 

注意:

強烈不建議使用finalize()函數進行任何操作!如果需要釋放資源,請使用try-finally。

因爲finalize()不確定性大,開銷大,無法保證順利執行。

 

3 方法區內存的回收

 

由於方法區中存放生命週期較長的類信息、常量、靜態變量,因此方法區就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉。

 

方法區中主要清除兩種垃圾:

1.廢棄常量

2. 廢棄的類

 

3.1 如何判定廢棄常量?

 

清除廢棄的常量和清除對象類似,只要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除掉。

 

3.2 如何清除廢棄的類?

 

清除廢棄類的條件較爲苛刻:

1.該類的所有對象都已被清除

2. 該類的java.lang.Class對象沒有被任何對象或變量引用 ,無法在任何地方通過反射訪問該類的方法。

只要一個類被虛擬機加載進方法區,那麼在堆中就會有一個代表該類的對象:java.lang.Class。這個對象在類被加載進方法區的時候創建,在方法區中該類被刪除時清除。

3.加載該類的ClassLoader已經被回收

 

4 垃圾收集算法

 

4.1 標記-清除Mark-Sweep算法

 

首先利用上面的方法判斷需要清除哪些數據,並給它們做上標記;然後清除被標記的數據。

 

 

缺點:

①標記和清除過程效率都很低;

②清除完後存在大量碎片空間,導致找不到連續內存來存儲大對象,從而不得不提前觸發另一次垃圾收集動作。

 

4.2 複製(Copying)算法

 

將內存分成兩份,只將數據存儲在其中一塊上。當需要回收垃圾時,也是首先標記出廢棄的數據,然後將有用的數據複製到另一塊內存上,最後將第一塊內存全部清除。這樣來回拷貝,始終保持有一個區域爲空。

 

優點:

  • 高效:

    ①整片內存一次性清理

    ②不用標記

        對於新生代中要回收很多垃圾對象這樣的操作,若使用標記清除算法會很耗時間,而複製算法只需要複製少量的存活對象便能將一整塊連續內存清除掉,這樣的操作在計算機中是很快的,很省時間;

 

  • 避免了碎片空間。

 

4.3 標記-整理(Mark-Compact)算法

 

標記:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷GC Roots,然後將存活的對象標記。

整理:移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收。因此,第二階段才稱爲整理階段。直接清理掉邊界以外的內存。爲什麼需要整理?因爲當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

分析:

優點:避免內存碎片,不用內存減半。

它是一種老年代的垃圾收集算法。老年代中的對象一般壽命比較長,因此每次垃圾回收會有大量對象存活,因此如果選用“複製”算法,每次需要複製大量存活的對象,會導致效率很低。而且,在新生代中使用“複製”算法,當Eden+Survivor中都裝不下某個對象時,可以使用老年代的內存進行“分配擔保”,而如果在老年代使用該算法,那麼在老年代中如果出現Eden+Survivor裝不下某個對象時,沒有其他區域給他作分配擔保。因此,老年代中一般使用“標記-整理”算法。

 

4.4 分代收集算法

 

將內存劃分爲老年代和新生代(經常1:3)。

新生代包含一個eden區,兩個survivor區(又是也成爲FromTo區),默認比例8:1:1 (可以調節)

老年代中存放壽命較長的對象,新生代中存放“朝生夕死”的對象。然後在不同的區域使用不同的垃圾收集算法。

新生代:特徵:存活很少,垃圾很多。採用複製算法,少量複製,批量抹除。

老年代:特徵:存活很多,沒有分配擔保。採用標記-壓縮算法,避免碎片。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章