文章目錄
- 1、引用計數算法(Reference Counting)
- 2、可達性分析算法(GC Roots Analysis):
- 3.finalize()方法最終判定對象是否存活
- 1).第一次标記并進行一次篩選。
- 2).第二次标記
堆中幾乎存放着Java世界中所有的對象執行個體,垃圾收集器在對堆回收之前,第一件事情就是要确定這些對象哪些還“存活”着,哪些對象已經“死去”(即不可能再被任何途徑使用的對象)。
1、引用計數算法(Reference Counting)
很多教科書判斷對象是否存活的算法是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值加1;當引用失效時,計數器減1;任何時刻計數器都為0的對象就是不可能再被使用的。
引用計數算法的實作簡單,判斷效率也很高,在大部分情況下它都是一個不錯的算法。但是Java語言中沒有選用引用計數算法來管理記憶體,其中最主要的一個原因是它很難解決對象之間互相循環引用的問題。
例如:
在testGC()方法中,對象objA和objB都有字段instance,指派令objA.instance=objB及objB.instance=objA,除此之外這兩個對象再無任何引用,實際上這兩個對象都已經不能再被通路,但是它們因為互相引用着對象方,異常它們的引用計數都不為0,于是引用計數算法無法通知GC收集器回收它們。
列印GC詳細資訊:
-XX:+PrintGCDetails
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0gTMx81dsQWZ4lmZf1GLlpXazVmcvwFciV2dsQXYtJ3bm9CX9s2RkBnVHFmb1clWvB3MaVnRtp1XlBXe0xCMy81dvRWYoNHLwEzX5xCMx8FesU2cfdGLwMzX0xiRGZkRGZ0Xy9GbvNGLpZTY1EmMZVDUSFTU4VFRR9Fd4VGdsYTMfVmepNHLrJXYtJXZ0F2dvwVZnFWbp1zczV2YvJHctM3cv1Ce-cmbw5iN2YjNxEGNwQ2YxkzY0UzYyYzXxITO0YTM2AzLcdDMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.png)
idea配置: Run -> Run configurations -> java應用名 -> arguments ->VM arguments,加入jvm參數就行
輸入VM arguments參數:
-
-Xms20m --jvm堆的最小值
Xmx20m --jvm堆的最大值
-
-XX:+PrintGCTimeStamps -- 列印出GC的時間資訊
-
XX:+PrintGCDetails --列印出GC的詳細資訊
-
verbose:gc --開啟gc日志
-
Xloggc:d:/gc.log -- gc日志的存放位置
-
Xmn10M -- 新生代記憶體區域的大小
XX:SurvivorRatio=8 --新生代記憶體區域中Eden和Survivor的比例
/**
* 執行後,objA和objB會不會被GC呢?
*/
public class ReferenceCountingGC {
public Object instance = null;
/**
* 這個成員屬性的唯一意義就是占點記憶體,以便能在GC日志中看清楚是否被回收過
*/
private byte[] bigSize = new byte[2 * 1024 * 1024];
public static void main(String[] args) {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//假設在這行發生了GC,objA和ojbB是否被回收
System.gc();
//1.8采用Parallel GC
}
}
[GC (Allocation Failure) [DefNew: 3707K->512K(4928K), 0.0041893 secs] 3707K->1214K(15872K), 0.0042359 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [Tenured: 702K->1216K(10944K), 0.0039473 secs] 5529K->1216K(15872K), [Metaspace: 2223K->2223K(4480K)], 0.0039998 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 4992K, used 45K [0x04600000, 0x04b60000, 0x09b50000)
eden space 4480K, 1% used [0x04600000, 0x04612f10, 0x04a60000)
from space 512K, 0% used [0x04a60000, 0x04a60000, 0x04ae0000)
to space 512K, 0% used [0x04ae0000, 0x04ae0000, 0x04b60000)
tenured generation total 10944K, used 1216K [0x09b50000, 0x0a600000, 0x14600000)
the space 10944K, 11% used [0x09b50000, 0x09c80108, 0x09c80200, 0x0a600000)
Metaspace used 2228K, capacity 2280K, committed 2368K, reserved 4480K
在運作結果中可以看到:
GC日志中包含"3707K->512K",老年代從3707K(大約3.5M,其實就是objA與objB)變為了512K,意味着虛拟并沒有因為這兩個對象互相引用就不回收它們,這也
證明虛拟機并不是通過通過引用計數算法來判斷對象是否存活的
。
大家可以看到對象進入了老年代,
但是大家都知道,對象剛建立的時候是配置設定在新生代中的,要進入老年代預設年齡要到了new objA才行,但這裡objA與objB卻進入了老年代。
這是因為Java堆區會動态增長,剛開始時堆區較小,對象進入老年代還有一規則,當Survior空間中同一代的對象大小之和超過Survior空間的一半時,對象将直接進行老年代。
2、可達性分析算法(GC Roots Analysis):
主流用這個判斷。
在主流的商用程式語言中(Java和C#),都是使用可達性分析算法判斷對象是否存活的。這個算法的基本思路就是通過一系列名為"GC Roots"的對象作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連時,則證明此對象是不可用的,下圖對象object5, object6, object7雖然有互相判斷,但它們到GC Roots是不可達的,是以它們将會判定為是可回收對象。
在Java語言裡,可作為GC Roots對象的包括如下幾種:
- a.虛拟機棧(棧桢中的本地變量表)中的引用的對象
- b.方法區中的類靜态屬性引用的對象
- c.方法區中的常量引用的對象
- d.本地方法棧中JNI的引用的對象
3.finalize()方法最終判定對象是否存活
即使在可達性分析算法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經曆再次标記過程。
标記的前提是對象在進行可達性分析後發現沒有與GC Roots相連接配接的引用鍊。
1).第一次标記并進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法。
當對象沒有覆寫finalize方法,或者finzlize方法已經被虛拟機調用過,虛拟機将這兩種情況都視為“沒有必要執行”,對象被回收。
2).第二次标記
如果這個對象被判定為有必要執行finalize()方法,那麼這個對象将會被放置在一個名為:F-Queue的隊列之中,并在稍後由一條虛拟機自動建立的、低優先級的Finalizer線程去執行。這裡所謂的“執行”是指虛拟機會觸發這個方法,但并不承諾會等待它運作結束。這樣做的原因是,如果一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的情況),将很可能會導緻F-Queue隊列中的其他對象永久處于等待狀态,甚至導緻整個記憶體回收系統崩潰。
Finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC将對F-Queue中的對象進行第二次小規模标記,如果對象要在finalize()中成功拯救自己----隻要重新與引用鍊上的任何的一個對象建立關聯即可,譬如把自己指派給某個類變量或對象的成員變量,那在第二次标記時它将移除出“即将回收”的集合。如果對象這時候還沒逃脫,那基本上它就真的被回收了。
流程圖如下:
/**
* 此代碼示範了兩點
* 1、對象可以在被GC時自我拯救
* 2、這種自救的機會隻有一次,因為一個對象的finalize()方法最多隻能被系統自動調用一次。
*/
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, I am still alive");
}
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
//對象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//因為finalize方法優先級很低,所有暫停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no ,I am dead QAQ!");
}
//-----------------------
//以上代碼與上面的完全相同,但這次自救卻失敗了!!!
SAVE_HOOK = null;
System.gc();
//因為finalize方法優先級很低,所有暫停0.5秒以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no ,I am dead QAQ!");
}
}
}
輸出:
finalize method executed!
yes, I am still alive
no ,I am dead QAQ!
了解這篇文章,還要有對finalize的認識: