其實(shí)我以前看到提到 JVM 之類的東西,就覺得這玩意太難讯检,看不下去啊⌒粒可是要進(jìn)階吧视哑,又是必須的啦,所以說(shuō)一句 干就完了
今天就來(lái)學(xué)學(xué) GC誊涯,想問就我一個(gè)人想到中文的滾粗嗎
其實(shí)這么理解也可以啦,GC 是什么意思呢蒜撮,可以大概這么理解 公廁只有這么些個(gè)暴构,偏偏有些人占著茅廁不那啥跪呈,只好讓管理員讓它滾粗啦,好讓其它人可以用取逾,讓世界和平耗绿。
好了好了 回來(lái)了回來(lái)了,為什么會(huì)有 GC (回收垃圾砾隅,釋放內(nèi)存)呢误阻?因?yàn)閮?nèi)存就這么大,用完了不回收晴埂,就一直占用著究反,可以用的內(nèi)存就越來(lái)越小,到最后溢出了(OOM)儒洛,程序也就GG了精耐。
那么問題來(lái)了,怎么知道這個(gè)占用著的對(duì)象是不是垃圾呢琅锻?
一:引用計(jì)數(shù)法
給對(duì)象添加一個(gè)引用計(jì)數(shù)器卦停,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器就加1恼蓬,當(dāng)引用失效時(shí)惊完,計(jì)數(shù)器就減 1,任何時(shí)刻計(jì)數(shù)器為 0 的對(duì)象就是不可能再被使用的处硬,就是垃圾了小槐,可以回收了。(但是主流的虛擬機(jī)并不是用這種方法)
二:可達(dá)性分析算法
講解這個(gè)之前郁油,我覺得還是要先知道 Java 運(yùn)行時(shí)的內(nèi)存區(qū)域是怎么區(qū)分的本股。自行百度?哈哈
網(wǎng)上找了張圖:
什么意思呢桐腌?
意思就是拄显,這個(gè)算法呢就是通過一系列稱為 “GC Roots” 的對(duì)象作為起點(diǎn),從這些起點(diǎn)向下搜索案站,搜索走過的路徑稱為 引用鏈躬审,當(dāng)一個(gè)對(duì)象到 GC Roots 沒有引用鏈的時(shí)候,我們就可以指著它的鼻子罵“垃圾”蟆盐,等著被回收吧承边?
那么就有人懵逼了 GC Roots 是什么玩意啊,我不知道啊
舉個(gè)我認(rèn)為的例子:看過戰(zhàn)狼2的都知道石挂,有了中國(guó)的護(hù)照博助,我們就是祖國(guó)花朵(哈哈),到哪都是中國(guó)人的身份痹愚,那么國(guó)家就是我們最強(qiáng)的后盾富岳。那么我們就可以把 中國(guó)理解為 GC Roots,護(hù)照就是篩選是否是中國(guó)人的 引用鏈蛔糯,有就有中國(guó)做我們的后盾,沒有就等著被回收(沒被的意思....)
那么在程序中 什么可以作為 GC Roots呢窖式?
- 虛擬機(jī)棧中的引用
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量的引用對(duì)象
- 本地方法棧中 JNI(Native) 引用的對(duì)象
什么時(shí)候回收
不同的虛擬機(jī)實(shí)現(xiàn)有著不同的 GC 實(shí)現(xiàn)機(jī)制蚁飒,但是一般情況下每一種 GC 實(shí)現(xiàn)都會(huì)在以下兩種情況下觸發(fā)垃圾回收。
Allocation Failure:在堆內(nèi)存中分配時(shí)萝喘,如果因?yàn)榭捎檬S嗫臻g不足導(dǎo)致對(duì)象內(nèi)存分配失敗淮逻,這時(shí)系統(tǒng)會(huì)觸發(fā)一次 GC。
System.gc():在應(yīng)用層阁簸,Java 開發(fā)工程師可以主動(dòng)調(diào)用此 API 來(lái)請(qǐng)求一次 GC爬早。
那么這里就引出了 引用 的概念,全部有4種引用
- 強(qiáng)引用:
就是代碼中直接 new 對(duì)象的引用强窖,這種是永遠(yuǎn)不會(huì)被回收的 - 軟引用:
用來(lái)描述一些非必須的對(duì)象凸椿,弱引用的對(duì)象 將在內(nèi)存將要發(fā)生內(nèi)存溢出異常之前,會(huì)被列進(jìn)回收范圍進(jìn)行二次回收翅溺,如果回收之后內(nèi)存還不夠脑漫,才報(bào)溢出異常 - 弱引用:
也是用來(lái)描述一些非必須的對(duì)象,但是它是 無(wú)論內(nèi)存夠不夠都會(huì)被會(huì)回收 - 虛引用:
僅持有虛引用的對(duì)象咙崎,在任何時(shí)候都可能被GC优幸,設(shè)置虛引用的唯一目的:就是能在這個(gè)對(duì)象唄回收時(shí)收到一個(gè)系統(tǒng)通知。
注意:即使在可達(dá)性分析算法中不可達(dá)的對(duì)象褪猛,也不是 非死不可的网杆,這時(shí)候它們暫時(shí)處于“緩刑”,真正死亡伊滋,至少要經(jīng)歷兩次標(biāo)記的過程:判斷對(duì)象是否有必要執(zhí)行finalize()方法碳却;若被判定為有必要執(zhí)行finalize()方法,之后還會(huì)對(duì)對(duì)象再進(jìn)行一次篩選笑旺,如果對(duì)象能在finalize()中重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)昼浦,將被移除出“即將回收”的集合。任何對(duì)象的 finalize()方法筒主,只會(huì)執(zhí)行一次关噪。
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 mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//對(duì)象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停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();
// 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停0.5秒藤韵,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
#################
運(yùn)行結(jié)果:
finalize mehtod execute!
yes, i am still alive :)
no, i am dead :(
兩段代碼是一樣的虐沥,執(zhí)行結(jié)果一次成功,一次失敗泽艘,這就是上面提到的任何對(duì)象的 finalize()方法置蜀,只會(huì)執(zhí)行一次奈搜。
那么問題來(lái)了悉盆,知道是垃圾后盯荤,要怎么去回收呢?當(dāng)然就是每個(gè)人都會(huì)說(shuō)的垃圾收集算法啦
-
標(biāo)記 — 清除算法
顧名思義焕盟,是垃圾 就 標(biāo)記秋秤,然后把標(biāo)記的給 清除 就好了(標(biāo)記過程就是上面講的)
缺點(diǎn):效率,空間問題脚翘。標(biāo)記清除后會(huì)產(chǎn)生大量的不連續(xù)內(nèi)存碎片灼卢。
為啥會(huì)有這樣的缺點(diǎn),不理解?舉個(gè)例子:
你種了好多大白菜来农,一眼看過去鞋真,好多個(gè)點(diǎn)都有壞死的,而且都不是同一個(gè)區(qū)域沃于,你依次走過去涩咖,標(biāo)記是要扔掉的,然后就去清理了繁莹,這樣下來(lái)效率就很慢了檩互,而且你拔掉的地方那么小,翻土重新種又太小咨演,容易弄壞其他的白菜闸昨,只能空著,那么就是空間問題了薄风。
復(fù)制算法
把可用內(nèi)存按容量劃分為大小相等的兩塊饵较,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用盡后遭赂,把還存活著的對(duì)象 復(fù)制 到另外一塊上面循诉,再將這一塊內(nèi)存空間一次清理掉。
例子:這個(gè)就更容易理解了嵌牺,叫了兩打啤酒打洼,每次喝都隨機(jī)兩打里面拿,要結(jié)賬的時(shí)候逆粹,把空瓶裝一箱募疮,沒喝存起來(lái)的裝一箱,結(jié)賬時(shí)候清理空瓶的那一箱就好了僻弹。
缺點(diǎn):為了解決效率問題阿浓。代價(jià) 內(nèi)存縮小了一半
標(biāo)記 — 整理算法
首先 標(biāo)記 出所有需要回收的對(duì)象,然后進(jìn)行 整理蹋绽,使得存活的對(duì)象都向一端移動(dòng)芭毙,最后直接清理掉端邊界以外的內(nèi)存筋蓖。
例子 :平常我們自己整理房間就是啦,雜亂的房間中 不想要的扔掉退敦,不要讓它繼續(xù)堆放在房間占位置粘咖,把空出來(lái)的存放自己想要的東西,
優(yōu)點(diǎn):即沒有浪費(fèi)50%的空間侈百,又不存在空間碎片問題瓮下,性價(jià)比較高。
一般情況下钝域,老年代會(huì)選擇標(biāo)記-整理算法讽坏。
分代收集算法
根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊。一般是把 JAVA 堆分為新生代和老年代例证,然后根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?br>
新生代:回收時(shí)路呜,有大批對(duì)象死去,只有少量存活织咧,適合復(fù)制算法胀葱。
老年代:存活率高,沒有額外的分配擔(dān)狈掣校空間巡社,必須使用 標(biāo)記 — 清理,或 標(biāo)記 — 整理手趣。