jvm 之 垃圾標記演算法
JVM深入筆記(3)垃圾標記演算法
- Author: Poechant
- Blog: blog.CSDN.net/Poechant
- Email: zhongchao.ustc#gmail.com (#->@)
- Date: March 3rd, 2012
- Copyright © 柳大·Poechant
如果您還不瞭解 JVM 的基本概念和記憶體劃分,請先閱讀《JVM 深入筆記(1)記憶體區域是如何劃分的?》一文。然後再回來 :)
因為 Java 中沒有留給開發者直接與記憶體打交道的指標(C++工程師很熟悉),所以如何回收不再使用的物件的問題,就丟給了 JVM。所以下面就介紹一下目前主流的垃圾收集器所採用的演算法。不過在此之前,有必要先講一下 Reference。
1 引用(Reference)
你現在還是 JDK 1.0 或者 1.1 版本的開發者嗎?如果是的話,可以告訴你跳過“5 Reference”這一部分吧,甚至跳過本文。如果不是的話,下面這些內容還是有參考價值的。你可能會問,Reference 還有什麼可講的?還是有一點的,你知道 Reference 有四種分類嗎?這可不是孔乙己的四種“回”字寫法可以類比的。說引用,我們最先想到的一般是:
Object obj = new Object();
這種屬於 Strong Reference(JDK 1.2 之後引入),這類 ref 的特點就是,只要 ref 還在,目標物件就不能被幹掉。我們可以想一下為什麼要幹掉一些物件?很簡單,因為記憶體不夠了。如果記憶體始終夠用,大家都活著就好了。所以當記憶體不夠時,會先幹掉一些“必死無疑的傢伙”(下面會解釋),如果這時候記憶體還不夠用,就該幹掉那些“可死可不死的傢伙”了。
JDK 1.2 之後還引入了 SoftReference 和 WeakReference,前者就是那些“可死可不死的傢伙”。當進行了一次記憶體清理(幹掉“必死無疑”的傢伙)後,還是不夠用,就再進行一次清理,這次清理的內容就是 SoftReference 了。如果幹掉 Soft Reference 後還是不夠用,JVM 就丟擲 OOM 異常了。
好像 WeakReference 還沒說呢?它是幹嘛的?其實它就是那些“必死無疑的傢伙”。每一次 JVM 進行清理時,都會將這類 ref 幹掉。所以一個 WeakReference 出生後,它的死期,就是下一次 JVM 的清理。
“回”字的最後一種寫法,是 PhantomReference,名字很恐怖吧(Phantom是鬼魂的意思,不僅含義恐怖,而且發音也恐怖——“墳頭”)。這類 ref 的唯一作用,就是當相應的 Object 被 clean 掉的時候,通知 JVM。
雖然有四種“回”字,但是 Strong Reference 卻沒有相應的類,java.lang.ref.Reference 只有三個子類。
你可能會發現,在 Reference 這一部分,我經常性地提到“清理”。什麼“清理”?就是下面要說的 Garbage Collection 中對”無用”物件的 clean。
這是 JVM 的核心功能之一,同時也是為什麼絕大多數 Java 工程師不需要像 C++ 程式設計師那樣考慮物件的生存期問題。至於因此而同時導致 Java 工程師不能夠放任自由地控制記憶體的結果,其實是一個 Freedom 與 Effeciency 之間的 trade-off,而 C++ 工程師與 Java 工程師恰如生存在兩個國度的人,好像“幸福生活”的天朝人民與“水深火熱”的西方百姓之間的“時而嘲笑、時而豔羨”一般。
言歸正傳,Garbage Collector(GC)是 JVM 中篩選並清理 Garbage 的工具。那麼第一個要搞清楚的問題是,什麼是 Garbage?嚴謹的說,Garbage 就是不再被使用、或者認為不再被使用、甚至是某些情況下被選作“犧牲品”的物件。看上去很羅嗦,那就先理解成“不再被使用”吧。這就出現了第二個問題,怎麼判斷不再被使用?這就是下面首先要介紹的 Object Marking Algorithms。
2 物件標記演算法(Object Marking Algorithms)
下面還是先從本質一點的東西開始說吧。一個物件變得 useless 了,其實就是它目前沒有稱為任何一個 reference 的 target,並且認為今後也不會成為(這是從邏輯上說,實際上此刻沒有被引用的物件,今後也沒有人會去引用了⋯⋯)
2.1 引用計數法(Reference Counting)
核心思想:很簡單。每個物件都有一個引用計數器,當在某處該物件被引用的時候,它的引用計數器就加一,引用失效就減一。引用計數器中的值一旦變為0,則該物件就成為垃圾了。但目前的 JVM 沒有用這種標記方式的。為什麼呢?
因為引用計數法無法解決迴圈引用(物件引用關係組成“有向有環圖”的情況,涉及一些圖論的知識,在根搜尋演算法中會解釋)的問題。比如下面的例子:
package com.sinosuperman.jvm;
class _1MB_Data {
public Object instance = null;
private byte[] data = new byte[1024 * 1024 * 1];
}
public class CycledReferenceProblem {
public static void main(String[] args) {
_1MB_Data d1 = new _1MB_Data();
_1MB_Data d2 = new _1MB_Data();
d1.instance = d2;
d2.instance = d1;
d1 = null;
d2 = null;
System.gc();
}
}
在這個程式中,首先在堆記憶體中建立了兩個 1MB 大小的物件,並且其中分別儲存的 instance 成員引用了對方。那麼即使 d1和 d2 被置為 null 時,引用數並沒有變為零。如果這是採用引用計數法來標記的話,記憶體就被浪費了,gc 的時候不會被回收。好悲催啊 :(
重複一下在《JVM 深入筆記(1)記憶體區域是如何劃分的?》中提到的執行環境:
**Mac OS X 10.7.3**,**JDK 1.6.0 Update 29**,**Oracle Hot Spot 20.4-b02**。
那麼我們來試試Oracle Hot
Spot 20.4-b02
是不是採用引用計數法來標記的。對了,別忘了為CycledReferenceProblem
使用的虛擬機器開啟-XX:+PrintGCDetails
引數,然後執行結果如下:
[Full GC (System) [CMS: 0K->366K(63872K), 0.0191521 secs] 3778K->366K(83008K), [CMS Perm : 4905K->4903K(21248K)], 0.0192274 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
Heap
par new generation total 19136K, used 681K [7f3000000, 7f44c0000, 7f44c0000)
eden space 17024K, 4% used [7f3000000, 7f30aa468, 7f40a0000)
from space 2112K, 0% used [7f40a0000, 7f40a0000, 7f42b0000)
to space 2112K, 0% used [7f42b0000, 7f42b0000, 7f44c0000)
concurrent mark-sweep generation total 63872K, used 366K [7f44c0000, 7f8320000, 7fae00000)
concurrent-mark-sweep perm gen total 21248K, used 4966K [7fae00000, 7fc2c0000, 800000000)
可以看到,在Full GC
時,清理掉了 (3778-366)KB=3412KB 的物件。這一共有 3MB 多,可以確定其中包括兩個我們建立的 1MB 的物件嗎?貌似無法確定。好吧,那下面我們使用_2M_Data
物件來重複上面的程式。
package com.sinosuperman.jvm;
class _2MB_Data {
public Object instance = null;
private byte[] data = new byte[1024 * 1024 * 2];
}
public class CycledReferenceProblem {
public static void main(String[] args) {
_2MB_Data d1 = new _2MB_Data();
_2MB_Data d2 = new _2MB_Data();
d1.instance = d2;
d2.instance = d1;
d1 = null;
d2 = null;
System.gc();
}
}
執行結果如下:
[Full GC (System) [CMS: 0K->366K(63872K), 0.0185981 secs] 5826K->366K(83008K), [CMS Perm : 4905K->4903K(21248K)], 0.0186886 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]
Heap
par new generation total 19136K, used 681K [7f3000000, 7f44c0000, 7f44c0000)
eden space 17024K, 4% used [7f3000000, 7f30aa4b0, 7f40a0000)
from space 2112K, 0% used [7f40a0000, 7f40a0000, 7f42b0000)
to space 2112K, 0% used [7f42b0000, 7f42b0000, 7f44c0000)
concurrent mark-sweep generation total 63872K, used 366K [7f44c0000, 7f8320000, 7fae00000)
concurrent-mark-sweep perm gen total 21248K, used 4966K [7fae00000, 7fc2c0000, 800000000)
這次清理掉了 (5826-366)=5460KB 的物件。我們發現兩次清理相差 2048KB,剛好是 2MB,也就是 d1 和 d2 剛好各相差 1MB。我想這可以確定,gc 的時候確實回收了兩個迴圈引用的物件。如果你還不信,可以再試試 3MB、4MB,都是剛好相差 2MB。
這說明oracle Hot
Spot 20.4-b02
虛擬機器並不是採用引用計數方法。事實上,現在沒有什麼流行的 JVM 會去採用簡陋而問題多多的引用計數法來標記。不過要承認,它確實簡單而且大多數時候有效。
那麼,這些主流的 JVM 都是使用什麼標記演算法的呢?
2.2. 根搜尋演算法(Garbage Collection Roots Tracing)
對,沒錯,就是“根搜尋演算法”。我來介紹以下吧。
2.2.1 基本思想
其實思路也很簡單(演算法領域,除了紅黑樹、KMP等等比較複雜外,大多數思路都很簡單),可以概括為如下幾步:
- 選定一些物件,作為 GC Roots,組成基物件集(這個詞是我自己造的,與其他文獻資料的說法可能不一樣。但這無所謂,名字只是個代號,理解演算法內涵才是根本);
- 由基物件集內的物件出發,搜尋所有可達的物件;
- 其餘的不可達的物件,就是可以被回收的物件。
這裡的“可達”與“不可達”與圖論中的定義一樣,所有的物件被看做點,引用被看做有向連線,整個引用關係就是一個有向圖。在“引用計數法”中提到的迴圈引用,其實就是有向圖中有環的情況,即構成“有向有環圖”。引用計數法不適用於“有向有環圖”,而根搜尋演算法適用於所有“有向圖”,包括有環的和無環的。那麼是如何解決的呢?
2.2.2 GC Roots
如果你的邏輯思維夠清晰,你會說“一定與選取基物件集的方法有關”。是的,沒錯。選取 GC Roots 組成基物件集,其實就是選取如下這些物件:
《深入理解 Java 虛擬機器:JVM 高階特性與最佳實踐》一書中提到的 GC Roots 為:
- 方法區(Method Area,即 Non-Heap)中的類的 static 成員引用的物件,和 final 成員引用的物件;
- Java 方法棧(Java Method Stack)的區域性變數表(Local Variable Table)中引用的物件;
- 原生方法棧(Native Method Stack)中 JNI 中引用的物件。
但顯然不夠全面,[參考2]中提到的要更全面:(March 6th,2012 update)
- 由系統類載入器載入的類相應的物件:這些類永遠不會被解除安裝,且這些類建立的物件都是 static 的。注意使用者使用的類載入器載入的類建立的物件,不屬於 GC Roots,除非是 java.lang.Class 的相應例項有可能會稱為其他類的 GC Roots。
- 正在執行的執行緒。
- Java 方法棧(Java Method Stack)的區域性變數表(Local Variable Table)中引用的物件。
- 原生方法棧(Native Method Stack)的區域性變數表(Local Variable Table)中引用的物件。
- JNI 中引用的物件。
- 同步監控器使用的物件。
- 由 JVM 的 GC 控制的物件:這些物件是用於 JVM 內部的,是實現相關的。一般情況下,可能包括系統類載入器(注意與“1”不一樣,“1”中是 objects created by the classes loaded by system class loaders,這裡是 the objects, corresponding instances of system class loaders)、JVM 內部的一些重要的異常類的物件、異常控制程式碼的預分配物件和在類載入過程中自定義的類載入器。不幸的是,JVM 並不提供這些物件的任何額外的詳細資訊。因此這些實現相關的內容,需要依靠分析來判定。
所以這個演算法實施起來有兩部分,第一部分就是到 JVM 的幾個記憶體區域中“找物件”,第二部分就是運用圖論演算法。
3. 廢話
JVM 的標記演算法並不是 JVM 垃圾回收策略中最重要的。真正的核心,是回收演算法,當然標記演算法是基礎。如果你想複習一下前兩篇文章,連結在這裡:
JVM 深入筆記(1)記憶體區域是如何劃分的?
JVM 深入筆記(2)各記憶體區溢位場景模擬
JVM 深入筆記(3)垃圾標記演算法
參考
- http://www.yourkit.com/docs/10/help/gc_roots.jsp
- 《深入理解 Java 虛擬機器:JVM 高階特性與最佳實踐》周志明(著),機械工業出版社
-
如果這篇文章幫助到了您,歡迎您到我的部落格留言,我會很高興的。
轉載請註明來自“柳大的CSDN部落格”:blog.csdn.net/Poechant
-
相關文章
- JVM 深入筆記(3)垃圾標記演算法JVM筆記演算法
- JVM垃圾回收之三色標記JVM
- JVM之垃圾回收(1-概述+演算法)JVM演算法
- JVM調優之垃圾定位、垃圾回收演算法、垃圾處理器對比JVM演算法
- JVM垃圾回收演算法JVM演算法
- 【JVM】JVM系列之垃圾回收(二)JVM
- JVM系列(五) - JVM垃圾回收演算法JVM演算法
- JVM 垃圾回收演算法和垃圾回收器JVM演算法
- 【JVM之記憶體與垃圾回收篇】堆JVM記憶體
- JVM(九):垃圾回收演算法JVM演算法
- JVM記憶體分配策略,及垃圾回收演算法JVM記憶體演算法
- 垃圾回收演算法|GC標記-清除演算法演算法GC
- JVM效能調優-演算法內功之剖析標記清除JVM演算法
- JVM讀書筆記之垃圾收集與記憶體分配JVM筆記記憶體
- jvm(三)——jvm垃圾回收演算法以及實現JVM演算法
- 深入探究JVM之垃圾回收演算法實現細節JVM演算法
- jvm有哪些垃圾回收演算法JVM演算法
- 理解JVM(二):垃圾收集演算法JVM演算法
- jvm垃圾分代回收演算法JVM演算法
- 垃圾回收的標記清除演算法詳解演算法
- 【JVM之記憶體與垃圾回收篇】JVM與Java體系結構JVM記憶體Java
- Java教程分享:JVM垃圾回收機制之物件回收演算法JavaJVM物件演算法
- 深入探究JVM之垃圾回收器JVM
- JVM調優:基本垃圾回收演算法JVM演算法
- JVM-垃圾收集演算法基礎JVM演算法
- Golang 垃圾回收-三色標記清除演算法Golang演算法
- 物件回收判定與垃圾回收演算法-JVM學習筆記(1)物件演算法JVM筆記
- Java記憶體管理 -JVM 垃圾回收Java記憶體JVM
- JVM記憶體管理和垃圾回收JVM記憶體
- 【JVM】垃圾回收的四大演算法JVM演算法
- 深入理解JVM(四)——垃圾回收演算法JVM演算法
- 【JVM之記憶體與垃圾回收篇】虛擬機器棧JVM記憶體虛擬機
- JVM垃圾回收JVM
- jvm - 垃圾回收JVM
- [JVM]垃圾回收JVM
- ☕[JVM技術指南](2)垃圾回收子系統(Garbage Collection System)之常見的垃圾回收演算法JVM演算法
- 各種垃圾回收演算法(二)標記-清除( Mark-Sweep )演算法演算法
- 深入理解 JVM 之 垃圾收集器JVM