對(duì)象是"存活"還是"已死"
在堆里面存放著Java世界中幾乎所有的對(duì)象實(shí)例泪幌,垃圾收集器在對(duì)堆進(jìn)行回收前肴焊。第一件事情就是確定這些對(duì)象之中哪些還"存活"其屏,哪些是"已死"(即不可能在
被任何途徑使用的對(duì)象)
- 引用計(jì)數(shù)法
- 可達(dá)性分析算法
- 生存還是死亡
- 回收方法區(qū)
引用計(jì)數(shù)法
很多教科書判斷對(duì)象是否存活的算法是這樣的:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí)措译,計(jì)數(shù)器就加1别凤;當(dāng)引用失效時(shí),計(jì)數(shù)器就減1领虹;任何時(shí)刻
計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的规哪。</br>
客觀地說,引用計(jì)數(shù)算法的實(shí)現(xiàn)簡(jiǎn)單塌衰,判斷效率很高诉稍,在大部分情況下它都是一個(gè)不錯(cuò)的算法,也有一些比較著名的應(yīng)用案例最疆。但是杯巨,至少主流的Java虛擬機(jī)里
面沒有使用引用計(jì)數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對(duì)象之間相互循環(huán)引用的問題努酸。</br>
如下測(cè)試代碼:Test0.class
/**
* * * * 社
* * * * * 會(huì)
* * * * * * 主義
* * * * * 碼
* * * * 農(nóng)
* VM Args: -Xmx10m -Xms10m -XX:+PrintGCDetails -XX:+UseParNewGC
* @author FeiYun
**/
public class Test1 {
public Object instance = null;
private static final int ONE_MB = 1024 * 1024;
//為了占內(nèi)存
private byte[] bigSize = new byte[2 * ONE_MB];
public static void testGC() {
Test1 objA = new Test1();
Test1 objB = new Test1();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
//在這里進(jìn)行GC服爷,看objA和objB是否會(huì)被回收
System.gc();
}
public static void main(String[] args) {
Test1.testGC();
}
}
輸出結(jié)果:
[Full GC (System.gc()) [Tenured: 2554K->467K(6848K), 0.0014870 secs] 4729K->467K(9920K), [Metaspace: 2988K->2988K(1056768K)], 0.0015045 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
par new generation total 3072K, used 97K [0x00000007bf600000, 0x00000007bf950000, 0x00000007bf950000)
eden space 2752K, 3% used [0x00000007bf600000, 0x00000007bf6185d8, 0x00000007bf8b0000)
from space 320K, 0% used [0x00000007bf8b0000, 0x00000007bf8b0000, 0x00000007bf900000)
to space 320K, 0% used [0x00000007bf900000, 0x00000007bf900000, 0x00000007bf950000)
tenured generation total 6848K, used 467K [0x00000007bf950000, 0x00000007c0000000, 0x00000007c0000000)
the space 6848K, 6% used [0x00000007bf950000, 0x00000007bf9c4d08, 0x00000007bf9c4e00, 0x00000007c0000000)
Metaspace used 3004K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 328K, capacity 388K, committed 512K, reserved 1048576K
從運(yùn)行結(jié)果中可以清楚看到,GC日志中包含"4729K->467K",意味著虛擬機(jī)并沒有因?yàn)檫@兩個(gè)對(duì)象互相引用就不回收它們层扶,這也從側(cè)面說明虛擬機(jī)并不是通過引
用計(jì)數(shù)算法來判斷對(duì)象是否存活的。
可達(dá)性分析算法
在主流的商用程序語言的主流實(shí)現(xiàn)中烙荷,都是稱通過可達(dá)性分析(Reachability Analysis)來判定對(duì)象是否存活的镜会。這個(gè)算法的基本思路就是通過一系列的稱
為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始鄉(xiāng)下搜索终抽,搜索所走過的路徑稱為引用鏈(Reference Chain)戳表,當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈
相連時(shí),則證明此對(duì)象是不可用的昼伴。
<img src="https://thumbnail1.baidupcs.com/thumbnail/7e811edd4od7b825869a052cb9b084a2?fid=1080334218-250528-592287742302570&rt=pr&sign=FDTAER-DCb740ccc5511e5e8fedcff06b081203-s6YILUzhndggiR0kQGh05ZAad7g%3d&expires=8h&chkbd=0&chkv=0&dp-logid=31134765181005687&dp-callid=0&time=1666850400&size=c1920_u1080&quality=90&vuk=1080334218&ft=image&autopolicy=1">
在Java語言中匾旭,可作為GC Roots的對(duì)象包括下面幾種:
- 虛擬機(jī)棧(棧幀中的本地變量)中引用的對(duì)象。
- 方法區(qū)中靜態(tài)屬性引用的對(duì)象圃郊。
- 方法區(qū)中常量引用的對(duì)象价涝。
- 本地方法棧中JNI(即一般說的Native方法)引用的對(duì)象。
再談引用
無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量持舆,還是通過可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá)色瘩,判斷對(duì)象是否存活都與"引用"有關(guān)。在JDK1.2以前逸寓,Java
中的引用的定義很傳統(tǒng):如果reference類型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址居兆,就稱這塊內(nèi)存代表著一個(gè)引用。這種定義很純粹竹伸,但是太
過狹隘泥栖,一個(gè)對(duì)象在這種定義下只有被引用或者沒有被引用兩種狀態(tài),對(duì)于如何描述一些"食之無味棄之可惜"的對(duì)象就顯得無能為力勋篓。</br>
在JDK1.2之后吧享,Java對(duì)引用的概念進(jìn)行了補(bǔ)充,將引用分為強(qiáng)引用(Strong Reference)譬嚣、軟引用(Soft Reference)耙蔑、弱引用(Weak Reference)、
虛引用(Phantom Reference)4種孤荣,這四種引用強(qiáng)度依次逐漸減弱甸陌。
- 強(qiáng)引用就是指在程序代碼之中普遍存在的,類似"Object obj = new Object()"這類的引用盐股,只要強(qiáng)引用還存在钱豁,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的
對(duì)象- 軟引用是用來描述一些還有用但并非必須的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象疯汁,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前牲尺,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第
二次回收男杈,如果這次回收沒有足夠的內(nèi)存越妈,才會(huì)拋出內(nèi)存溢出異常。在JDK1.2之后,提供了SoftReference類來實(shí)現(xiàn)軟引用竞漾。- 弱引用也是用來描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些休溶,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前府阀。當(dāng)垃圾收集器工作時(shí),無
論當(dāng)前內(nèi)存是否足夠搓茬,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象犹赖。在JDK1.2之后,提供了WeakReference類來實(shí)現(xiàn)弱引用卷仑。- 虛引用也被稱為幽靈引用或者幻影引用峻村,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在锡凝,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響粘昨,也無法通過虛引用來
取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收的時(shí)候收到一個(gè)系統(tǒng)通知窜锯。在JDK1.2之后雾棺,提供了PhantomReference
類來實(shí)現(xiàn)虛引用。
生存還是死亡
即使在可達(dá)性分析算法中不可達(dá)的對(duì)象衬浑,也并非"非死不可"的捌浩,這時(shí)候它們暫時(shí)處于"緩行"階段,要真正宣告一個(gè)對(duì)象死亡工秩,至少要經(jīng)歷兩次標(biāo)記過程:
- 第一次標(biāo)記過程:如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈尸饺,那它會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選。篩選的條件是此對(duì)象是否
有必要執(zhí)行finalize()方法助币。當(dāng)對(duì)象沒有覆蓋finalize()方法浪听,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為"沒有必要執(zhí)行"
如果對(duì)象被判斷定位有必要執(zhí)行finalize()方法眉菱,那個(gè)這個(gè)對(duì)象會(huì)被放置在一個(gè)叫做F-Queue的隊(duì)列中迹栓,并在稍后有一個(gè)虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的Finalizer
線程去執(zhí)行它俭缓。這里所謂的"執(zhí)行"是指迅即會(huì)觸發(fā)這個(gè)方法克伊,但并不會(huì)承諾會(huì)等待它運(yùn)行結(jié)束,這個(gè)樣做的原因是华坦,如果一個(gè)對(duì)象在finalize()方法中執(zhí)行
緩慢愿吹,或者發(fā)生死循環(huán),將可能會(huì)導(dǎo)致F-Queue隊(duì)列中其他對(duì)象永久處于等待惜姐,甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰犁跪。finalize()方法是對(duì)象逃脫死亡命運(yùn)的最
后一次機(jī)會(huì)椿息,稍后GC會(huì)對(duì)F-Queue中的對(duì)象進(jìn)行二次標(biāo)記。- 第二次標(biāo)記過程:如果對(duì)象在finalize()中成功拯救自己——只要重新與引用鏈上的任何對(duì)象建立關(guān)聯(lián)即可坷衍,譬如把自己(this關(guān)鍵字)賦值給某個(gè)類變量
或者對(duì)象的成員變量寝优,那在第二次標(biāo)記時(shí)它將被移除出"即將回收"的集合;如果對(duì)象這個(gè)時(shí)候還沒有逃脫死亡的命運(yùn)枫耳,那基本上它就真的被回收了乏矾。
如下測(cè)試代碼:Test2.class
/**
* * * * 社
* * * * * 會(huì)
* * * * * * 主義
* * * * * 碼
* * * * 農(nóng)
* 1、對(duì)象可以在GC時(shí)被自救
* 2嘉涌、自救只能有一次妻熊,因?yàn)閷?duì)對(duì)象的finalizer()方法只會(huì)被系統(tǒng)調(diào)用一次
* @author FeiYun
**/
public class Test2 {
public static Test2 instance = null;
public void isAlive() {
System.out.println("yes I am still alive:");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed夸浅!");
Test2.instance = this;
}
public static void main(String[] args) throws Throwable {
instance = new Test2();
//對(duì)象第一次成功拯救自己
instance = null;
System.gc();
//因?yàn)閒inalizer()方法執(zhí)行優(yōu)先級(jí)很低仑最,所以等待0.5秒
Thread.sleep(500);
if (instance != null) {
instance.isAlive();
} else {
System.out.println("no I am dead :(");
}
//嘗試對(duì)象第二次成功拯救自己
instance = null;
System.gc();
Thread.sleep(500);
if (instance != null) {
instance.isAlive();
} else {
System.out.println("no I am dead :(");
}
}
}
輸出結(jié)果:
finalize method executed!
yes I am still alive:
no I am dead :(
從結(jié)果可以看出帆喇,instance對(duì)象的finalizer()方法確實(shí)被GC收集器觸發(fā)過警医,并且在被收集前成功逃脫了,但是需要說明的是finalizer()方法只會(huì)被系統(tǒng)自
動(dòng)調(diào)用一次坯钦,如果對(duì)象面臨下一次回收预皇,它的finalizer()方法不會(huì)再被執(zhí)行,因此第二段代碼的自救行動(dòng)失敗了婉刀。
回收方法區(qū)
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類吟温。
- 廢棄常量
回收廢棄常量與回收J(rèn)ava堆中的對(duì)象非常類似。以常量池中字面量的回收為例突颊,假如一個(gè)字符串"abc"已經(jīng)進(jìn)入了常量池中鲁豪,但是當(dāng)前系統(tǒng)沒有任何一個(gè)
String對(duì)象叫做"abc"的,換句話說律秃,就是沒有任何String對(duì)象引用常量池中的"abc"常量爬橡,也沒有其他地方引用這個(gè)字面量,如果這時(shí)發(fā)生內(nèi)存回收棒动,而
且必要的話糙申,這個(gè)"abc"常量就會(huì)被系統(tǒng)清理出常量池。常量池中的其他類(接口)船惨、方法柜裸、字段的符號(hào)引用也與此類似。
- 廢棄無用的類
判斷一個(gè)常量是否是"廢棄常量"比較簡(jiǎn)單粱锐,而要判定是個(gè)類是否是"無用的類"的條件則相對(duì)苛刻許多粘室。類需要同時(shí)滿足下面三個(gè)條件才能算是"無用的類":
- 該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該累的任何實(shí)例卜范。
- 加載類的ClassLoader已經(jīng)被回收衔统。
- 該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機(jī)可以對(duì)滿足上述3個(gè)條件的無用類進(jìn)行回收锦爵,這里說的僅僅是"可以"舱殿,而并不是和對(duì)象一樣,不使用了就必然回收险掀。是否對(duì)類進(jìn)行回收沪袭,HotSpot虛擬機(jī)
提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading樟氢、-XX:+TraceClassUnLoading查看類加載和卸載信息冈绊,
其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機(jī)中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機(jī)埠啃。