jvm JAVA GC之標記

  堆分爲年輕代和年老代。永久代是非堆內存,它又叫做方法區(一般的說法),主要存儲已被加載的類信息、常量、靜態變量。而該區域在java8已被刪除,取而代之的是元空間,我會在後面的章節細講。

什麼是標記?怎麼標記?

  第一個問題相信大家都知道,標記就是對一些已死的對象打上記號,方便垃圾收集器的清理。 至於怎麼標記,一般有兩種方法:引用計數和可達性分析。

  引用計數實現起來比較簡單,就是給對象添加一個引用計數器,每當有一個地方引用它時就加1,引用失效時就減1,當計數器爲0的時候就標記爲可回收。這種判斷效率很高,但是很多主流的虛擬機並沒有採用這種方法,主要是因爲它很難解決幾個對象之間循環引用的問題,像下圖這個例子這樣發生循環引用。雖然不怎麼用了,但還是值得我們學習!

  可達性分析的基本思路就是:通過將一些稱爲”GC Roots”的對象作爲起始點,從這些節點開始搜索,搜索和該節點發生直接或者間接引用關係的對象,將這些對象以鏈的形式組合起來,形成一張“關係網”,又叫做引用鏈。最後垃圾收集器就回收那些不在這張關係網上的對象。如圖:

可達性分析

  連接GC Roots對象的object是確定還存活的對象,而右邊的die obj由於和GCROOTS沒有關係,所以會標記爲可回收的對象。目前主流的商用虛擬機用的都是類似的方法。那什麼對象才能作爲“GC Roots”呢?在java中,有四種對象可以作爲“GC Roots”

  1. 棧幀(第一章的名詞)中的引用對象。(棧中的)
  2. 靜態屬性引用的對象。(方法區中的)
  3. 常量引用的對象。(方法區中的)
  4. 本地方法棧中JNI引用的對象。(本地方法棧中的)

      目前來說這兩種標記的方法最爲常用,帶標記完成後就可以進行上一章所講的回收方法了!

鮮爲人知的二次標記

我們講到了標記,但是不是被標記了就肯定會被回收呢?不知道小夥伴們記不記得Object類有一個finalize()方法,所有類都繼承了Object類,因此也默認實現了這個方法。

  這個方法的用途就是:在該對象被回收之前,該對象的finalize()方法會被調用。這裏的回收之前指的就是被標記之後,問題就出在這裏,有沒有一種情況就是原本一個對象開始不在上一章所講的“關係網”(引用鏈)中,但是當開發者重寫了finalize()後,並且將該對象重新加入到了“關係網”中,也就是說該對象對我們還有用,不應該被回收,但是已經被標記啦,怎麼辦呢?

  針對這個問題,虛擬機的做法是進行兩次標記,即第一次標記不在“關係網”中的對象。第二次的話就要先判斷該對象有沒有實現finalize()方法了,如果沒有實現就直接判斷該對象可回收;如果實現了就會先放在一個隊列中,並由虛擬機建立的一個低優先級的線程去執行它,隨後就會進行第二次的小規模標記,在這次被標記的對象就會真正的被回收了。我們來看下面的代碼:

  嘿嘿,其實面對同一個對象,他的finalize()方法只會被調用一次,因此第一次調用的時候會進行finalize()方法,並且成功的將該對象加入了“關係網”中,但當第二次回收的時候並不會進入,所以第二次不能將對象加入“關係網”中,導致被回收了。

   圖中有一行讓程序睡眠一秒鐘的代碼,爲的就是確保讓低優先級的執行finalize()方法線程執行完成。那如果我們把他註釋了會怎樣呢?輸出結果是:

  很奇怪吧,不過如果執行很多次的話,也會出現最開始那樣的結果,但多數會是這個結果。因爲我們已經說了,執行finalize()的是一個低優先級的線程,既然是一個新的線程,雖然優先級低了點,但也是和垃圾收集器併發執行的,所以垃圾收集器沒必要等這個低優先級的線程執行完才繼續執行。也就是說,finalize()方法不一定會在對象第一次標記後執行。用一句清晰易懂的話來說就是:虛擬機確實有調用方法的動作,但是不會確保在什麼時候執行完成。因此也就出現了上面輸出的結果,對象被回收之後,那個低優先級的線程才執行完。

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