一、 如何定位垃圾
- 垃圾回收的核心工作就是回收垃圾泡一,在JVM 的視角來看:垃圾就是無用的對象占用的堆內(nèi)存空間颤殴。那么如何定位垃圾便是垃圾回收的重中之重。
1. 引用計數(shù)算法(reference counting)
- 算法思想:給對象中添加一個引用計數(shù)器鼻忠,每當有一個地方引用它時涵但,計數(shù)器就加 1;當引用失效時,計數(shù)器值就減 1矮瘟;一旦某個對象的引用計數(shù)器為 0瞳脓,則說明該對象已經(jīng)死亡,便可被回收澈侠。
- 缺陷:無法處理循環(huán)引用對象劫侧,如下:
/**
* testGC() 方法執(zhí)行后,objA 和 objB是否會被GC埋涧?
* ClassName: ReferenceCountingGC
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
對象 a, b 相互引用板辽,除此之外沒有其他引用指向a 或者 b,在這種情況下棘催,a 和 b 實際已經(jīng)死亡劲弦,但是由于他們的引用計數(shù)器皆不為 0 ,在引用計數(shù)算法的心中醇坝,這兩個對象還活著邑跪。因此,這些循環(huán)引用對象所占據(jù)的空間將不可回收呼猪,從而造成內(nèi)存泄漏画畅。
-
GC Roots: 可以理解為由堆外指向堆內(nèi)的引用,包括(但不限于)如下幾種:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象宋距;
- 方法區(qū)中類靜態(tài)屬性引用的對象轴踱;
- 方法區(qū)中常量引用的對象;
- 本地方法棧中JNI(即Native 方法)引用的對象谚赎;
- 已啟動且未停止的 Java 線程淫僻。
2. 可達性分析算法
- 算法思想:以“GC Roots”的對象作為起始點,若無法到達對象 a 或者 b壶唤,則可達性分析便不會將它們加入存活對象合集中雳灵。其中搜索走過的路徑稱為引用鏈(Reference Chain)
- 缺陷:在多線程環(huán)境下,其他線程可能會更新已經(jīng)訪問過的對象中的引用闸盔,從而造成誤報(將引用設置為 null)或漏報(將引用設置為被訪問的對象)
- 誤報只會使JVM 損失了部分垃圾回收的機會悯辙,即當GC標記完成,還未開始回收迎吵,你更新了其中一個引用躲撰,使之指向 null,那么原來的指向?qū)ο蟊究梢员换厥眨ǖ珱]有被GC 標記為可回收击费,只能等待下次標記)茴肥。
- 漏報是已經(jīng)被 GC 標記為可回收的對象,更新為被其他對象指向荡灾,垃圾回收器直接給回收掉了瓤狐,則可能會直接導致JVM 崩潰瞬铸。
3. Stop-the-world 以及安全點
- 目的是為了解決上述標記過程中堆棧的狀態(tài)發(fā)生改變,JVM 采取安全點機制來實現(xiàn) Stop-the-world 操作础锐,暫停其他非垃圾回收線程嗓节。因此老年代回收有卡頓現(xiàn)象。
4. Java的四種引用:強軟弱虛
5. 對象的最后一次自我拯救
- 即使在可達性算法中不可達的對象皆警,仍有一次自我拯救的機會拦宣。因為宣告一個對象的死亡至少要經(jīng)歷兩次標記過程:對象在可達性算法標記為不可達后進行一次篩選,判斷此對象是否有必要執(zhí)行 finalize() 方法信姓。當對象沒有覆蓋 finalize() 方法鸵隧,或者 finalize() 方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都是為“沒有必要執(zhí)行”意推。
- 在重寫的 finalize() 方法中豆瘫,只要重新與引用鏈上的任何一個對象建立關聯(liá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;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 對象第一次成功自我拯救
SAVE_HOOK = null;
System.gc();
// 由于 finalize() 方法優(yōu)先級很低菊值,所以暫停 0.5秒等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 對象第二次自我拯救 失斖馇!
SAVE_HOOK = null;
System.gc();
// 由于 finalize() 方法優(yōu)先級很低腻窒,所以暫停 0.5秒等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
運行結(jié)果:
finalize method executed!
yes, i am still alive:)
no, i am dead :(
執(zhí)行結(jié)果一次成功自救昵宇,一次失敗,這是因為任何一個對象的 finalize() 方法都只會被系統(tǒng)自動調(diào)用一次儿子,如果對象面臨下一次回收瓦哎,它的 finalize() 方法不會再次執(zhí)行。
二柔逼、如何回收垃圾
- 當標記完所有的存活對象是杭煎,便可以對死亡的對象進行回收了
1. 標記 - 清除(Mark-Sweep)
-
算法思想:把死亡對象所占據(jù)的內(nèi)存標記為空閑內(nèi)存,并記錄在空閑列表(free list)之中卒落。當需要新建對象時,內(nèi)存管理模塊便會從該空閑列表中尋找空閑內(nèi)存蜂桶,并劃分給新建的對象儡毕。
Sweep - 缺點:
- 造成內(nèi)存碎片。由于JVM 的堆中對象必須是連續(xù)分布的扑媚,因此可能出現(xiàn)總空閑內(nèi)存足夠腰湾,但是無法分配的極端情況。
- 分配效率較低疆股。如果是一塊連續(xù)的內(nèi)存空間费坊,那么可以通過指針加法(pointer bumping)來做分配。而對于空閑列表旬痹,JVM 則需要逐個訪問列表的項附井,來查找能夠放入新建對象的空閑內(nèi)存讨越。
2. 壓縮(compact)也叫標記 - 整理(Mark-Compact)
-
算法思想:把存活的對象聚集到內(nèi)存區(qū)域的起始位置,從而留下一段連續(xù)的內(nèi)存空間永毅。
Compact - 優(yōu)缺點:能夠解決內(nèi)存碎片化的問題把跨,代價就是壓縮算法的性能開銷。
3. 復制(copy)
-
算法思想: 把內(nèi)存區(qū)域分為兩等分沼死,分別用兩個指針 from 和 to 來 維護着逐,并且只是用 from 指針指向的內(nèi)存區(qū)域來分配內(nèi)存。當發(fā)生垃圾回收時意蛀,便把存活的對象復制到to 指針指向的內(nèi)存區(qū)域中耸别,并且交換 from 指針 和 to 指針的內(nèi)容。
Copy 優(yōu)缺點: 能夠解決內(nèi)存碎片化的問題县钥,但堆空間的使用效率極其低下秀姐。
4. 分代收集(Generational Collection)
- 算法思想:根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般為新生代和 老年代魁蒜,便可根據(jù)各個年代的特點采用最適合的收集算法囊扳。在新生代中,每次垃圾收集時都會有大批對象死去兜看,只有少量存活锥咸,便采用復制算法,而老年代中因為對象存活率高细移、沒有額外空間對它進行分配擔保搏予,則使用 “標記 - 清除” 或者 “標記 -整理” 算法。
5. JVM 的堆劃分
- 前面提到了新生代 和 老年代弧轧,這是 JVM 對堆的劃分雪侥,其中新生代又被劃分為 Eden 區(qū),以及兩個大小相同的 Survivor 區(qū)。
- 默認情況下,JVM采取的是一種動態(tài)分配的策略(對應JVM參數(shù) -XX:UsePSAdaptiveSurvivorSizePolicy)螟蝙,根據(jù)生成對象的速率跨细,以及 Survivor 區(qū)使用情況動態(tài)調(diào)整 Eden區(qū) 和 Survivor 卻的比例。
-
當然,也可以通過參數(shù) -XX:SurvivorRatio 來固定這個比例。不過Survivor區(qū)會一直為空,因此比例越低浪費的堆空間將越高原茅。
堆空間
- 當 調(diào)用new 指令時,它會在 Eden 區(qū)中劃出一塊作為存儲對象的內(nèi)存堕仔。由于堆空間是線程共享的擂橘,因此直接在這里劃空間是需要進行同步的。不然可能出現(xiàn)兩個對象公用一段內(nèi)存的事故摩骨。
- JVM 的解決方法就是:每個線程都可以向JVM 申請一段連續(xù)的內(nèi)存通贞,作為線程私有的TLAB(線程私有緩沖區(qū) Thread Local Allocation Buffer朗若,對應虛擬機參數(shù) -XX:+UseTLAB,默認開啟)滑频。這個操作需要加鎖捡偏,線程需要維護兩個指針(可能更多,主要的就兩個)峡迷,一個指向TLAB 中空余內(nèi)存的起始位置银伟,一個則指向 TLAB 末尾。
- 接下來的new 指令绘搞,便可以直接通過指針加法(bump the pointer)來實現(xiàn)彤避,即把指向空余內(nèi)存位置的指針加上所請求的字節(jié)數(shù)。如果加法后空余內(nèi)存指針的值仍小于或等于指向末尾的指針夯辖,則代表分配成功琉预。否則,TLAB 已經(jīng)沒有足夠的空間來滿足本次新建操作蒿褂。此時圆米,便需要當前線程重新申請新的 TLAB。
- 如果 Eden 區(qū)的空間耗盡啄栓,此時JVM 會觸發(fā)一次 Minor GC娄帖,來收集新生代的垃圾。存活下來的對象昙楚,則會被送到 Survivor 區(qū)近速。新生代有兩個 Survivor 區(qū),我們分別用 from 和 to來指代堪旧。其中 to 指向 Survivor 區(qū)是空的削葱。
- 當發(fā)生 Minor GC時,Eden 區(qū)和 from指向的 Survivor 區(qū)中的存活對象會被復制到 to指向的 Survivor 區(qū)中淳梦,然后交換 from 和 to 指針析砸,以保證下一次 Minor GC時,to 指向的Survivor 區(qū)還是空的爆袍。
- 新生代和老年代的劃分: JVM會記錄 Survivor 區(qū)中的對象一共被來回復制了幾次首繁。如果一個對象被復制的次數(shù)為 15(對應虛擬機參數(shù) -XX:+MaxTenuringThreshold),那么該對象將被晉升(promote)至老年代螃宙。另外,如果 單個 Survivor 區(qū)已經(jīng)被占用了 50%(對應虛擬機參數(shù) -XX:TargetSurvivorRatio)所坯,那么較高復制次數(shù)的對象也會被晉升至老年代谆扎。
注:為什么是15 而不是其他?原因是 HotSpot會在對象頭中的標記字段里記錄年齡芹助,分配到的空間只有4位堂湖,因此只能記錄到15
- 優(yōu)缺點:Minor GC 是不用對整個堆進行垃圾回收闲先,此時有一個問題就是老年代的對象可能引用新生代的對象。也就是說无蜂,在標記存活對象的時候伺糠,我們需要掃描老年代中的對象。如果該對象擁有對新生代對象的引用斥季,你們這個引用也會被作為 GC Roots训桶。如此,豈不是又做了一次全堆掃描酣倾?
6. 卡表(Card Table)
HotSpot為了解決Minor GC的時候不用進行全堆掃描而提供的方案
- 原理:該技術將整個堆劃分為一個個大小為512 字節(jié)的卡舵揭,并維護一個卡表,用來存儲每張卡的一個標識位躁锡。這個標識位代表對應的卡是否可能存有指向新生代對象的引用午绳。如果存在,你們這張卡就是臟的映之。
- 在進行 Minor GC的時候拦焚,便可不用掃描整個老年代,而是在卡表中尋找臟卡杠输,并將臟卡中的對象加入到 Minor GC 的 GC Roots中赎败。當完成所有臟卡的掃描后,JVM便可將所有的臟卡的標識位清零抬伺。
三螟够、 垃圾回收器
- 針對新生代的垃圾回收器有三個:Serial、Parallel Scavenge 和 Parallel New峡钓。都是采用標記 - 復制算法妓笙。其中,Serial 是單線程的能岩,Parallel New 可以看成 Serial 的多線程版本寞宫。Parallel Scavenge 和 Parallel New 類似,但更加注重吞吐量(Throughput)拉鹃,吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)辈赋。此外,Parallel Scavenge 不能與 CMS 一起使用膏燕。
- 針對老年代的垃圾回收器也有三個:Serial Old 和 Parallel Old钥屈,以及 CMS。Serial Old 和 Paralled Old 都是標記 - 壓縮算法坝辫。前者為單線程的篷就,后者可以看成前者的多線程版本。
- CMS 采用的是標記 - 清除算法近忙,并發(fā)收集竭业、低停頓智润。但是對CPU敏感、而且會產(chǎn)生空間碎片未辆。
- G1(Garbage First)是一個橫跨新生代和老年代的垃圾回收器窟绷。它打亂了前面所說的堆結(jié)構(gòu),直接將堆分為極多個區(qū)域咐柜。每個區(qū)域都可以充當 Eden 區(qū)兼蜈、Survivor 區(qū)或者老年代中的一個。采用的是“標記 - 壓縮”算法炕桨,而且和 CMS 一樣都能在應用程序運行過程中并發(fā)的進行垃圾回收饭尝。