簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處扎唾,謝謝营密!
前言
與C語言不同澄步,Java內(nèi)存(堆內(nèi)存)的分配與回收由JVM垃圾收集器自動完成冰蘑,這個特性深受大家歡迎,能夠幫助程序員更好的編寫代碼驮俗,本文以HotSpot虛擬機為例懂缕,說一說Java GC的那些事。
Java堆內(nèi)存
在 JVM內(nèi)存的那些事 一文中王凑,我們已經(jīng)知道Java堆是被所有線程共享的一塊內(nèi)存區(qū)域搪柑,所有對象實例和數(shù)組都在堆上進行內(nèi)存分配。為了進行高效的垃圾回收索烹,虛擬機把堆內(nèi)存劃分成新生代(Young Generation)工碾、老年代(Old Generation)和永久代(Permanent Generation)3個區(qū)域。
新生代
新生代由 Eden 與 Survivor Space(S0百姓,S1)構(gòu)成渊额,大小通過-Xmn參數(shù)指定,Eden 與 Survivor Space 的內(nèi)存大小比例默認為8:1,可以通過-XX:SurvivorRatio 參數(shù)指定旬迹,比如新生代為10M 時火惊,Eden分配8M,S0和S1各分配1M奔垦。
Eden:希臘語屹耐,意思為伊甸園,在圣經(jīng)中椿猎,伊甸園含有樂園的意思惶岭,根據(jù)《舊約·創(chuàng)世紀》記載,上帝耶和華照自己的形像造了第一個男人亞當(dāng)犯眠,再用亞當(dāng)?shù)囊粋€肋骨創(chuàng)造了一個女人夏娃按灶,并安置他們住在了伊甸園。
大多數(shù)情況下筐咧,對象在Eden中分配鸯旁,當(dāng)Eden沒有足夠空間時,會觸發(fā)一次Minor GC嗜浮,虛擬機提供了-XX:+PrintGCDetails參數(shù)羡亩,告訴虛擬機在發(fā)生垃圾回收時打印內(nèi)存回收日志。
Survivor:意思為幸存者危融,是新生代和老年代的緩沖區(qū)域畏铆。
當(dāng)新生代發(fā)生GC(Minor GC)時,會將存活的對象移動到S0內(nèi)存區(qū)域吉殃,并清空Eden區(qū)域辞居,當(dāng)再次發(fā)生Minor GC時,將Eden和S0中存活的對象移動到S1內(nèi)存區(qū)域蛋勺。
存活對象會反復(fù)在S0和S1之間移動瓦灶,當(dāng)對象從Eden移動到Survivor或者在Survivor之間移動時,對象的GC年齡自動累加抱完,當(dāng)GC年齡超過默認閾值15時贼陶,會將該對象移動到老年代,可以通過參數(shù)-XX:MaxTenuringThreshold 對GC年齡的閾值進行設(shè)置巧娱。
老年代
老年代的空間大小即-Xmx 與-Xmn 兩個參數(shù)之差碉怔,用于存放經(jīng)過幾次Minor GC之后依舊存活的對象。當(dāng)老年代的空間不足時禁添,會觸發(fā)Major GC/Full GC撮胧,速度一般比Minor GC慢10倍以上。
永久代
在JDK8之前的HotSpot實現(xiàn)中老翘,類的元數(shù)據(jù)如方法數(shù)據(jù)芹啥、方法信息(字節(jié)碼锻离,棧和變量大小)墓怀、運行時常量池汽纠、已確定的符號引用和虛方法表等被保存在永久代中,32位默認永久代的大小為64M傀履,64位默認為85M疏虫,可以通過參數(shù)-XX:MaxPermSize進行設(shè)置,一旦類的元數(shù)據(jù)超過了永久代大小啤呼,就會拋出OOM異常。
虛擬機團隊在JDK8的HotSpot中呢袱,把永久代從Java堆中移除了官扣,并把類的元數(shù)據(jù)直接保存在本地內(nèi)存區(qū)域(堆外內(nèi)存),稱之為元空間羞福。
這樣做有什么好處惕蹄?
有經(jīng)驗的同學(xué)會發(fā)現(xiàn),對永久代的調(diào)優(yōu)過程非常困難治专,永久代的大小很難確定卖陵,其中涉及到太多因素,如類的總數(shù)张峰、常量池大小和方法數(shù)量等泪蔫,而且永久代的數(shù)據(jù)可能會隨著每一次Full GC而發(fā)生移動。
而在JDK8中喘批,類的元數(shù)據(jù)保存在本地內(nèi)存中撩荣,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間,可以避免永久代的內(nèi)存溢出問題饶深,不過需要監(jiān)控內(nèi)存的消耗情況餐曹,一旦發(fā)生內(nèi)存泄漏,會占用大量的本地內(nèi)存敌厘。
ps:JDK7之前的HotSpot台猴,字符串常量池的字符串被存儲在永久代中,因此可能導(dǎo)致一系列的性能問題和內(nèi)存溢出錯誤俱两。在JDK8中饱狂,字符串常量池中只保存字符串的引用。
如何判斷對象是否存活
GC動作發(fā)生之前锋华,需要確定堆內(nèi)存中哪些對象是存活的嗡官,一般有兩種方法:引用計數(shù)法和可達性分析法。
1毯焕、引用計數(shù)法
在對象上添加一個引用計數(shù)器衍腥,每當(dāng)有一個對象引用它時磺樱,計數(shù)器加1,當(dāng)使用完該對象時婆咸,計數(shù)器減1竹捉,計數(shù)器值為0的對象表示不可能再被使用。
引用計數(shù)法實現(xiàn)簡單尚骄,判定高效块差,但不能解決對象之間相互引用的問題。
public class GCtest {
private Object instance = null;
private static final int _10M = 10 * 1 << 20;
// 一個對象占10M倔丈,方便在GC日志中看出是否被回收
private byte[] bigSize = new byte[_10M];
public static void main(String[] args) {
GCtest objA = new GCtest();
GCtest objB = new GCtest();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
通過添加-XX:+PrintGC參數(shù)憨闰,運行結(jié)果:
[GC (System.gc()) [PSYoungGen: 26982K->1194K(75776K)] 26982K->1202K(249344K), 0.0010103 secs]
從GC日志中可以看出objA和objB雖然相互引用,但是它們所占的內(nèi)存還是被垃圾收集器回收了需五。
2鹉动、可達性分析法
通過一系列稱為 “GC Roots” 的對象作為起點,從這些節(jié)點開始向下搜索宏邮,搜索路徑稱為 “引用鏈”泽示,以下對象可作為GC Roots:
- 本地變量表中引用的對象
- 方法區(qū)中靜態(tài)變量引用的對象
- 方法區(qū)中常量引用的對象
- Native方法引用的對象
當(dāng)一個對象到 GC Roots 沒有任何引用鏈時,意味著該對象可以被回收蜜氨。
在可達性分析法中械筛,判定一個對象objA是否可回收,至少要經(jīng)歷兩次標記過程:
1飒炎、如果對象objA到 GC Roots沒有引用鏈埋哟,則進行第一次標記。
2郎汪、如果對象objA重寫了finalize()方法定欧,且還未執(zhí)行過,那么objA會被插入到F-Queue隊列中怒竿,由一個虛擬機自動創(chuàng)建的砍鸠、低優(yōu)先級的Finalizer線程觸發(fā)其finalize()方法。finalize()方法是對象逃脫死亡的最后機會耕驰,GC會對隊列中的對象進行第二次標記爷辱,如果objA在finalize()方法中與引用鏈上的任何一個對象建立聯(lián)系,那么在第二次標記時朦肘,objA會被移出“即將回收”集合饭弓。
看看具體實現(xiàn)
public class FinalizerTest {
public static FinalizerTest object;
public void isAlive() {
System.out.println("I'm alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("method finalize is running");
object = this;
}
public static void main(String[] args) throws Exception {
object = new FinalizerTest();
// 第一次執(zhí)行,finalize方法會自救
object = null;
System.gc();
Thread.sleep(500);
if (object != null) {
object.isAlive();
} else {
System.out.println("I'm dead");
}
// 第二次執(zhí)行媒抠,finalize方法已經(jīng)執(zhí)行過
object = null;
System.gc();
Thread.sleep(500);
if (object != null) {
object.isAlive();
} else {
System.out.println("I'm dead");
}
}
}
執(zhí)行結(jié)果:
method finalize is running
I'm alive
I'm dead
從執(zhí)行結(jié)果可以看出:
第一次發(fā)生GC時弟断,finalize方法的確執(zhí)行了,并且在被回收之前成功逃脫趴生;
第二次發(fā)生GC時阀趴,由于finalize方法只會被JVM調(diào)用一次昏翰,object被回收。
當(dāng)然了刘急,在實際項目中應(yīng)該盡量避免使用finalize方法棚菊。
END。
我是占小狼叔汁。
在魔都艱苦奮斗统求,白天是上班族,晚上是知識服務(wù)工作者据块。
如果讀完覺得有收獲的話码邻,記得關(guān)注和點贊哦。
非要打賞的話另假,我也是不會拒絕的冒滩。