1 判斷對象是否存活
-
引用計數(shù)算法
給對象中添加一個引用計數(shù)器,每當有一個地方引用它時衅澈,計數(shù)器值就加1嗅榕;當引用失效時,計數(shù)器值就減1火焰;任何時刻計數(shù)器為0的對象就是不可能再被引用的劲装。主流的Java虛擬機里面沒有選用引用計數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間相互循環(huán)引用的問題昌简。 -
可達性分析算法
在主流的商用程序語言的主流實現(xiàn)中占业,都是通過可達性分析來判定對象是否存活。這個算法的基本思想就是通過一系列的稱為“GC Roots”的對象作為起始點江场,從這些節(jié)點開始向下搜索纺酸,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時址否,則證明此對象是不可用的餐蔬。
在Java語言中,可作為GC Roots的對象包括:
(1)虛擬機棧(棧幀中的本地變量表)中引用的對象佑附。
(2)方法區(qū)中的類靜態(tài)屬性引用的對象樊诺。
(3)方法區(qū)中的常量引用的對象。
(4)本地方法棧中JNI(即一般說的Native方法)的引用對象音同。
2 四種引用
-
強引用
強引用就是指在程序代碼之中普遍存在的词爬,類似“Object obj = new Object()”這類的引用,只要強引用還存在权均,垃圾收集器永遠不會回收掉被引用的對象顿膨。
public class StrongReferenceTest {
public static int M = 1024 * 1024;
public static void printlnMemory(String tag) {
Runtime runtime = Runtime.getRuntime();
int M = StrongReferenceTest.M;
System.out.println("\n" + tag + ":");
System.out.println(runtime.freeMemory() / M + "M(free)/" + runtime.totalMemory() / M + "M(total)");
}
public static void main(String[] args) {
StrongReferenceTest.printlnMemory("1.原可用內(nèi)存和總內(nèi)存");
// 實例化10M的數(shù)組锅锨,和strongReference建立強引用
byte[] strongReference = new byte[10 * StrongReferenceTest.M];
StrongReferenceTest.printlnMemory("2.實例化10M的數(shù)組,建立強引用");
System.out.println("strongReference : " + strongReference);
System.gc();
StrongReferenceTest.printlnMemory("3.GC之后");
System.out.println("strongReference : " + strongReference);
// 斷開強引用
strongReference = null;
StrongReferenceTest.printlnMemory("4.斷開強引用");
System.out.println("strongReference : " + strongReference);
System.gc();
StrongReferenceTest.printlnMemory("5.GC之后");
System.out.println("strongReference : " + strongReference);
}
}
1.原可用內(nèi)存和總內(nèi)存:
14M(free)/15M(total)
2.實例化10M的數(shù)組恋沃,建立強引用:
4M(free)/15M(total)
strongReference : [B@1b6d3586
3.GC之后:
4M(free)/15M(total)
strongReference : [B@1b6d3586
4.斷開強引用:
4M(free)/15M(total)
strongReference : null
5.GC之后:
14M(free)/15M(total)
strongReference : null
-
軟引用
軟引用是用來描述一些還有用但并非必需的對象抛虫。對于軟引用關(guān)聯(lián)著的對象选脊,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存笛洛,才會拋出內(nèi)存溢出異常惕味。在JDK1.2之后僚稿,提供了SoftReference類來實現(xiàn)軟引用蹦掐。
public class SoftReferenceTest {
public static int M = 1024 * 1024;
public static void printlnMemory(String tag) {
Runtime runtime = Runtime.getRuntime();
int M = StrongReferenceTest.M;
System.out.println("\n" + tag + ":");
System.out.println(runtime.freeMemory() / M + "M(free)/" + runtime.totalMemory() / M + "M(total)");
}
public static void main(String[] args) {
SoftReferenceTest.printlnMemory("1.原可用內(nèi)存和總內(nèi)存");
//建立軟引用
SoftReference<Object> softReference = new SoftReference<>(new byte[10 * SoftReferenceTest.M]);
SoftReferenceTest.printlnMemory("2.實例化10M的數(shù)組,建立軟引用");
System.out.println("softReference.get() : " + softReference.get());
System.gc();
SoftReferenceTest.printlnMemory("3.內(nèi)存可用容量充足户辞,GC后");
System.out.println("softReference.get() : " + softReference.get());
// 實例化一個4M的數(shù)組泌类,使內(nèi)存不夠用,建立軟引用
// GC后10M的數(shù)組被回收
SoftReference<Object> softReference2 = new SoftReference<>(new byte[4 * SoftReferenceTest.M]);
SoftReferenceTest.printlnMemory("4.實例化4M的數(shù)組咆课,建立軟引用");
System.out.println("softReference.get() : " + softReference.get());
System.out.println("softReference2.get() : " + softReference2.get());
}
}
1.原可用內(nèi)存和總內(nèi)存:
14M(free)/15M(total)
2.實例化10M的數(shù)組末誓,建立軟引用:
4M(free)/15M(total)
softReference.get() : [B@1b6d3586
3.內(nèi)存可用容量充足,GC后:
4M(free)/15M(total)
softReference.get() : [B@1b6d3586
4.實例化4M的數(shù)組书蚪,建立軟引用:
10M(free)/15M(total)
softReference.get() : null
softReference2.get() : [B@4554617c
-
弱引用
弱引用也是用來描述非必需對象的喇澡,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前殊校。當垃圾收集器工作時晴玖,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象为流。在JDK1.2之后呕屎,提供了WeakReference類來實現(xiàn)弱引用。
public class WeakReferenceTest {
public static int M = 1024 * 1024;
public static void printlnMemory(String tag) {
Runtime runtime = Runtime.getRuntime();
int M = WeakReferenceTest.M;
System.out.println("\n" + tag + ":");
System.out.println(runtime.freeMemory() / M + "M(free)/" + runtime.totalMemory() / M + "M(total)");
}
public static void main(String[] args) {
WeakReferenceTest.printlnMemory("1.原可用內(nèi)存和總內(nèi)存");
//創(chuàng)建弱引用
WeakReference<Object> weakReference = new WeakReference<Object>(new byte[10 * WeakReferenceTest.M]);
WeakReferenceTest.printlnMemory("2.實例化10M的數(shù)組敬察,并建立弱引用");
System.out.println("weakReference.get() : " + weakReference.get());
System.gc();
StrongReferenceTest.printlnMemory("3.GC后");
System.out.println("weakReference.get() : " + weakReference.get());
}
}
1.原可用內(nèi)存和總內(nèi)存:
14M(free)/15M(total)
2.實例化10M的數(shù)組秀睛,并建立弱引用:
4M(free)/15M(total)
weakReference.get() : [B@1b6d3586
3.GC后:
14M(free)/15M(total)
weakReference.get() : null
-
虛引用
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系莲祸。一個對象是否有虛引用的存在蹂安,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例锐帜。為一個對象設置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知田盈。在JDK1.2之后,提供了PhantomReference類來實現(xiàn)虛引用缴阎。
public class PhantomReferenceTest {
public static int M = 1024 * 1024;
public static void printlnMemory(String tag) {
Runtime runtime = Runtime.getRuntime();
int M = PhantomReferenceTest.M;
System.out.println("\n" + tag + ":");
System.out.println(runtime.freeMemory() / M + "M(free)/" + runtime.totalMemory() / M + "M(total)");
}
public static void main(String[] args) throws InterruptedException {
PhantomReferenceTest.printlnMemory("1.原可用內(nèi)存和總內(nèi)存");
byte[] object = new byte[10 * PhantomReferenceTest.M];
PhantomReferenceTest.printlnMemory("2.實例化10M的數(shù)組后");
// 建立虛引用
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(object, referenceQueue);
PhantomReferenceTest.printlnMemory("3.建立虛引用后");
System.out.println("phantomReference : " + phantomReference);
System.out.println("phantomReference.get() : " + phantomReference.get());
System.out.println("referenceQueue.poll() : " + referenceQueue.poll());
// 斷開強引用
object = null;
PhantomReferenceTest.printlnMemory("4.執(zhí)行object = null;強引用斷開后");
System.gc();
PhantomReferenceTest.printlnMemory("5.GC后");
System.out.println("phantomReference : " + phantomReference);
System.out.println("phantomReference.get() : " + phantomReference.get());
System.out.println("referenceQueue.poll() : " + referenceQueue.poll());
//斷開虛引用
phantomReference = null;
System.gc();
PhantomReferenceTest.printlnMemory("6.斷開虛引用后GC");
System.out.println("phantomReference : " + phantomReference);
System.out.println("referenceQueue.poll() : " + referenceQueue.poll());
}
}
1.原可用內(nèi)存和總內(nèi)存:
14M(free)/15M(total)
2.實例化10M的數(shù)組后:
4M(free)/15M(total)
3.建立虛引用后:
4M(free)/15M(total)
phantomReference : java.lang.ref.PhantomReference@1b6d3586
phantomReference.get() : null
referenceQueue.poll() : null
4.執(zhí)行object = null;強引用斷開后:
4M(free)/15M(total)
5.GC后:
4M(free)/15M(total)
phantomReference : java.lang.ref.PhantomReference@1b6d3586
phantomReference.get() : null
referenceQueue.poll() : java.lang.ref.PhantomReference@1b6d3586
6.斷開虛引用后GC:
14M(free)/15M(total)
phantomReference : null
referenceQueue.poll() : null
-
引用隊列(referenceQueue)
無實際存儲結(jié)構(gòu)允瞧,存儲邏輯依賴于內(nèi)部節(jié)點之間的關(guān)系來表達。
存儲關(guān)聯(lián)的并且被GC的軟引用、弱引用和虛引用述暂。
3 finalize方法
即使在可達性分析算法中不可達的對象痹升,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段贸典,要真正宣告一個對象死亡视卢,至少要經(jīng)歷兩次標記過程:如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選廊驼,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當對象沒有覆蓋finalize()方法惋砂,或者finalize()方法已經(jīng)被虛擬機調(diào)用過妒挎,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。
如果這個對象被判定為有必要執(zhí)行finalize()方法西饵,那么這個對象將會放置在一個叫做F-Queue的隊列之中酝掩,并在稍后由一個由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它眷柔。這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法期虾,但并不承諾會等待它運行結(jié)束,這樣做的原因是驯嘱,如果一個對象在finalize()方法中執(zhí)行緩慢镶苞,或者發(fā)生了死循環(huán)(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處于等待鞠评,甚至導致整個內(nèi)存回收系統(tǒng)崩潰茂蚓。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規(guī)模的標記剃幌,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可聋涨,譬如把自己(this關(guān)鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合负乡;如果對象這時候還沒有逃脫牍白,那基本上它就真的被回收了。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = 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!");
FinalizeEscapeGC.SAVE_HOOK = this;// 這里的this是指已經(jīng)進入F-Queue的那個FinalizeEscapeGC對象
System.out.println(SAVE_HOOK);
}
}
public class FinalizeEscapeGCTest {
public static void main(String[] args) throws Throwable {
FinalizeEscapeGC.SAVE_HOOK = new FinalizeEscapeGC();
System.out.println(FinalizeEscapeGC.SAVE_HOOK);
// 對象第一次成功拯救自己
FinalizeEscapeGC.SAVE_HOOK = null;
System.gc();
// 因為finalize方法優(yōu)先級很低抖棘,所以暫停0.5秒以等待它
Thread.sleep(500);
if (FinalizeEscapeGC.SAVE_HOOK != null) {
FinalizeEscapeGC.SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead");
}
// 下面這段代碼與上面的完全相同茂腥,但是這次自救卻失敗了
FinalizeEscapeGC.SAVE_HOOK = null;
System.gc();
// 因為finalize方法優(yōu)先級很低,所以暫停0.5秒以等待它
Thread.sleep(500);
if (FinalizeEscapeGC.SAVE_HOOK != null) {
FinalizeEscapeGC.SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead");
}
}
}
FinalizeEscapeGC@1b6d3586
finalize method executed!
FinalizeEscapeGC@1b6d3586
yes, i am still alive
no, i am dead
任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次钉答,如果對象面臨下一次回收础芍,它的finalize()方法不會被再次執(zhí)行,因此第二段代碼的自救行動失敗了数尿。
我們應該盡量避免使用finalize()方法仑性。
4 回收方法區(qū)
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類。
回收廢棄常量與回收Java堆中的對象非常類似右蹦。以常量池中字面量的回收為例诊杆,假如一個字符串“abc”已經(jīng)進入了常量池中歼捐,但是當前系統(tǒng)沒有任何一個String對象是叫做“abc”的,換句話說晨汹,就是沒有任何String對象引用常量池中的“abc”常量豹储,也沒有其他地方引用了這個字面量,如果這時發(fā)生內(nèi)存回收淘这,而且必要的話剥扣,這個“abc”常量就會被系統(tǒng)清理出常量池。常量池中的其他類(接口)铝穷、方法钠怯、字段的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單曙聂,而要判定一個類是否是“無用的類”的條件則相對苛刻許多晦炊。類需要同時滿足下面3個條件才能算是“無用的類”:
(1)該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例宁脊。
(2)加載該類的ClassLoader已經(jīng)被回收断国。
(3)該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法榆苞。
虛擬機可以對滿足上述3個條件的無用類進行回收稳衬,這里說的僅僅是“可以”,而并不是和對象一樣语稠,不使用了就必然會回收宋彼。
在大量使用反射、動態(tài)代理仙畦、CGLib等ByteCode框架输涕、動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出慨畸。
5 垃圾收集算法
-
標記-清除算法
標記-清除算法分為兩個階段:首先標記出所有需要回收的對象莱坎,在標記完成后統(tǒng)一回收所有被標記的對象。它的主要不足有兩個:一個是效率問題寸士,標記和清除兩個過程的效率都不高檐什;另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片弱卡,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時乃正,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。 -
復制算法
復制算法將可用內(nèi)存按容量劃分為大小相等的兩塊婶博,每次只使用其中一塊瓮具。當這一塊的內(nèi)存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉名党。這種算法的代價是將內(nèi)存縮小為了原來的一半叹阔,未免太高了一點。
現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代传睹。虛擬機將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間耳幢,每次使用Eden空間和其中一塊Survivor。當回收時欧啤,將Eden和Survivor中還存活著的對象一次性地復制到另外一塊Survivor空間上睛藻,最后清理掉Eden和剛才用過地Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1邢隧,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%修档,只有10%的內(nèi)存會被“浪費”。
我們沒有辦法保證每次回收都只有不多于10%的對象存活府框,當Survivor空間不夠用時,需要依賴老年代進行分配擔保讥邻。即如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時迫靖,這些對象將直接通過分配擔保機制進入老年代。 -
標記-整理算法
復制收集算法在對象存活率較高時就要進行較多的復制操作兴使,效率將會變低系宜。更關(guān)鍵的是,如果不想浪費50%的空間发魄,就需要有額外的空間進行分配擔保盹牧,以應對被使用的內(nèi)存中所有對象100%都存活的極端情況,所以在老年代一般不能直接選用這種算法励幼。
根據(jù)老年代的特點汰寓,有人提出了另外一種“標記-整理”算法,標記過程與“標記-清除”算法一樣苹粟,但后續(xù)步驟不是直接對可回收對象進行清理有滑,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存嵌削。 -
分代收集算法
當前商業(yè)虛擬機的垃圾收集都采用“分代收集”算法毛好,這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊苛秕。一般是把Java堆分為新生代和老年代肌访,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴ā?strong>在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去艇劫,只有少量存活吼驶,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高旨剥、沒有額外空間對它進行分配擔保咧欣,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。