1. 程式人生 > >jvm垃圾收集機制詳解(上)

jvm垃圾收集機制詳解(上)

在我們學習java之前,經常聽到的一個關於java的優點就是,相對於像C語言這種語言,省去了程式設計師手動回收垃圾的步驟,那麼,java虛擬機器到底是怎麼實現自動垃圾回收機制的呢?

一、如何判斷物件需要被回收

什麼時候需要回收物件?經常寫別的語言的人可能會說,當我們對一個東西使用完成,需要回收這個東西所佔用的空間的時候,需要去手動寫程式碼回收,例如在c++種呼叫物件的解構函式等的操作。對於java來說,回收記憶體空間的操作就是垃圾回收,也就是GC操作。

1.物件的死亡

GC操作預設狀況下是jvm虛擬機器自動進行的,它會確定“活著”的物件和“死去”的物件,死去的含義就是不可能再通過任何手段去使用,例如:

Person p = new Person();
p=null;

我們例項化了一個Person類的物件,然後把物件置為null,此時,p和我們剛才例項化的物件的引用關係就斷開了,那麼這個時候剛才的Person類例項物件就是一個“死去”的物件了,它將面臨被回收的命運。

2.如何判斷物件已死

2.1 引用計數演算法

演算法的名字很高大上,但是想法很簡單,我們可以給每一個物件都做一個標記,每當有一個地方引用了這個物件的時候,標記數就加1;當沒有地方引用這個物件的時候,標記也就為0,此時意味著這個物件應該被回收了。

演算法說起來很簡單,但是實際使用會發現一個問題,例如:

我們寫了一個類,熟悉資料結構的同學會發現這是連結串列的一個節點,裡面定義了這個節點儲存的資料和指向下一個節點的物件。

public class Node {
	int data;
	Node next;
}

下面我們來讓兩個節點互相指向,構成一個迴圈連結串列,然後把指向兩個節點的變數置為null

public class Test {
	public static void main(String[] args) {
		Node a = new Node();
		Node b = new Node();
		a.next = b;
		b.next = a;
		a=null;
		b=null;
	}
}

如果用圖去理解這個程式碼的話,就是下圖這樣:
在這裡插入圖片描述

我們發現,在斷開了a,b的連線之後,現在已經沒法通過任何方式獲取到這兩個迴圈指向的物件了,此時應該回收這兩個物件,但是由於這兩個物件互相引用的緣故,它們的引用標記都不是0,因此如果使用這種演算法的話面臨這種情況的時候沒法識別是否需要回收。

2.2 可達性分析演算法

由於第一種演算法存在的問題,在主流的商用程式語言的主流實現中,都是使用可達性分析演算法來判斷物件是否還活著。我們使用一種稱為GC Roots的物件,來檢測物件是否可以訪問,如果GC Roots和物件之間還有聯絡,說明物件不需要被回收,如果一個物件和任何的GC Roots之間都沒有了聯絡,則回收這個物件。

GC Roots說的神祕,其實就是我們上圖中說的a和b,a和b就是兩個GC Roots,當a和b這兩個GC Roots與物件之間斷開了連線,沒有任何其它的的GC Roots與兩個物件相連,此時這兩個物件將面臨被回收的命運。

可以作為GC Roots的物件包括四種:

  1. 虛擬機器棧(棧幀中的本地變量表)中引用的物件
  2. 方法區中類靜態屬性引用的物件
  3. 方法區中常量引用的物件
  4. 本地方法棧中Native方法引用的物件

其中,剛才我們的a和b就是第一種GC Roots

2.3 引用的分類

談到引用,你可能覺得自己已經很明白,不就是地址嘛,引用型別變數的意思無非就是一個變數儲存了虛擬機器中一塊區域的記憶體地址。但是,你可能不知道,引用其實有4種類型:

  1. 強引用

如果我們:

Object obj = new Object();

那麼這種引用就是強引用,在強引用存在的時候,物件不會被垃圾收集器回收。

  1. 軟引用
    軟引用是一些比較“”的引用,也就是比強引用要弱勢一些的引用,當jvm發現執行程式自己的記憶體不夠用的時候,會去找有沒有軟引用連線著的物件,如果有,就會把這些軟引用連線的物件進行二次回收;如果沒用軟引用物件或者回收了軟引用物件之後依然沒有物件,這個時候才會丟擲記憶體溢位異常。

  2. 弱引用
    弱引用也是用來描述類似軟引用的非必需物件的,但是它的強度比軟引用更弱,使用弱引用的物件在下次垃圾回收的時候必然被清除,無論記憶體是否夠用。

  3. 虛引用
    虛引用是最弱的一種引用關係,一個物件是否有虛引用的存在,完全不會影響它的存活時間,也無法通過虛引用來獲得一個物件例項,為一個物件設定虛引用關聯的唯一目的就是如果這個物件被回收了會給系統發一個通知。

3.物件是否必死無疑

一個物件沒有被GC Roots物件關聯,就一定要被回收,必死無疑嗎?

答案是否定的,哪可以隨隨便便就判死刑呢?還是要調查清楚走完流程才能判死刑的,要是萬一翻案了呢?

要真正給一個物件宣判死刑,要經過至少兩次標記過程:

  1. 如果物件被發現不可達,那麼物件會被第一次標記並進行一次篩選,如果物件沒有重寫finalize()方法或者虛擬機器已經呼叫過finalize()方法,那麼沒有必要再執行finalize()方法;除此以外的情況,物件將被判定為有必要執行finalize()方法,那麼將會進行下面的第二步
  2. 如果物件被判定為有必要執行finalize()方法,物件將會被放置在一個佇列中,稍後會由一個虛擬機器自動建立的低優先順序執行緒去執行這個物件的finalize()方法。虛擬機器會觸發這個方法,但是並不必須等待這個finalize()方法執行結束,因為如果一個物件的finalize()方法執行緩慢,或者發生了死迴圈(極端情況),那就可能會導致佇列中的其它物件等待時間過長,可能導致整個記憶體回收系統崩潰。finalize()方法是物件逃脫死刑的最後一次機會,如果物件想挽救自己,只需要在finalize()方法中重新將自己賦給某個變數即可。執行過finalize()方法之後,物件會被第二次標記,第二次標記之後,物件已經是必死無疑了。
    注意,一個物件的finalize()方法只能被呼叫一次,也就是說,我們在給物件賦值null然後呼叫GC回收之後,如果再次給物件賦值,然後再次給物件賦值null,然後再次呼叫GC回收,這時就不會第二次呼叫finalize()方法了

請儘量避免使用finalize(),這個方法畢竟只是一個c,c++遺留下來的妥協方法,請儘量忘記這個方法並在心中默唸三遍:“我是java程式設計師,我是java程式設計師,我是java程式設計師”

下一篇傳送門:jvm垃圾收集機制詳解(中)

  • 參考書籍:《深入理解Java虛擬機器》