一、簡(jiǎn)介
? ? (建議讀者了解Java虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域后再閱讀本章節(jié))
????在上一篇文章《淺談Java虛擬機(jī)(二)—運(yùn)行時(shí)數(shù)據(jù)區(qū)域》中,我們一起學(xué)習(xí)了Java虛擬機(jī)的內(nèi)存區(qū)域劃分以及每個(gè)區(qū)域的作用腰涧,并且知道了其中的虛擬機(jī)棧冗茸、本地方法棧茎毁、程序計(jì)數(shù)器是線程私有的祭犯,它們的生命周期是隨線程誕生和消亡,其中所占用的內(nèi)存會(huì)隨著消亡而自動(dòng)釋放统捶,因此榆芦,Java虛擬機(jī)不需要管理這部分的內(nèi)存。
????Java堆與方法區(qū)都是所有線程共享的區(qū)域喘鸟,其中Java堆作為Java虛擬機(jī)內(nèi)存中主要存放對(duì)象的區(qū)域匆绣,當(dāng)某個(gè)對(duì)象不會(huì)再被使用到時(shí),就需要Java虛擬機(jī)維護(hù)并清理這個(gè)對(duì)象所占有的內(nèi)存什黑,否則大量“無用”的對(duì)象堆積在Java堆中會(huì)很容易出現(xiàn)內(nèi)存溢出的現(xiàn)象崎淳,而這個(gè)回收“無用”對(duì)象的機(jī)制就稱為垃圾回收(Garbage Collection,以下簡(jiǎn)稱GC)機(jī)制愕把,實(shí)現(xiàn)這個(gè)機(jī)制的代碼模塊被稱為垃圾收集器拣凹。這里延伸補(bǔ)充一點(diǎn),垃圾回收并不是Java虛擬機(jī)的伴生產(chǎn)物礼华,更不是Java虛擬機(jī)獨(dú)有的特性咐鹤,事實(shí)上,垃圾回收的歷史比Java的誕生都更久遠(yuǎn)圣絮,只是Java使用了這個(gè)機(jī)制來管理內(nèi)存而已祈惶。
? ? 在上一章節(jié)的講解中提到過,方法區(qū)存儲(chǔ)了大量的常量扮匠、靜態(tài)變量捧请,在JDK1.7以前(此處仍然以主流商用虛擬機(jī)Sun HotSpot為例),其中的運(yùn)行時(shí)常量池還包含了字符串常量池棒搜,此時(shí)的方法區(qū)甚至是用永久代來實(shí)現(xiàn)疹蛉,方便GC的分代收集機(jī)制(下面會(huì)講到)回收此區(qū)域的內(nèi)存,然而力麸,Java虛擬機(jī)開發(fā)人員逐漸意識(shí)到可款,方法區(qū)中存放的不論常量、靜態(tài)變量還是字符串克蚂,可回收的價(jià)值都不高(它們大多都是整個(gè)程序運(yùn)行期間一直存在的闺鲸,回收價(jià)值相對(duì)較高的就是字符串),還浪費(fèi)了GC的性能埃叭,于是摸恍,逐步放棄用永久代實(shí)現(xiàn)方法區(qū),在JDK1.7時(shí),把字符串常量池放進(jìn)了Java堆中立镶,在JDK1.8時(shí)壁袄,使用元空間替代永久代實(shí)現(xiàn)方法區(qū),元空間在Java虛擬機(jī)之外媚媒,使用的本地內(nèi)存嗜逻,不易發(fā)生OOM,但同樣會(huì)觸發(fā)GC缭召,只是GC后會(huì)動(dòng)態(tài)擴(kuò)展元空間的內(nèi)存变泄,關(guān)于方法區(qū)更多細(xì)節(jié)可關(guān)注我的上一篇文章。
? ? 即使其他虛擬機(jī)對(duì)方法區(qū)的實(shí)現(xiàn)有所差異恼琼,Java堆仍是GC主要作用的目標(biāo)。
? ? GC如此智能屏富,那么我們?yōu)槭裁催€要了解它呢晴竞,答案引用《深入理解Java虛擬機(jī)—周志明》中的原話:
? ? 當(dāng)需要排查各種內(nèi)存溢出、內(nèi)存泄漏問題時(shí)狠半,當(dāng)垃圾收集稱為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí)噩死,我們就需要對(duì)這些“自動(dòng)化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)
? ? 下面將從GC是如何判斷對(duì)象可回收的,如何回收的(即有哪些回收算法)神年,以及何時(shí)觸發(fā)回收這幾個(gè)方面詳解垃圾回收機(jī)制已维。
二、如何判斷對(duì)象可回收
????垃圾回收的前提就是正確判斷可回收的對(duì)象已日,否則這個(gè)機(jī)制就亂套了垛耳。那么GC是如何判斷一個(gè)對(duì)象是否能回收呢,只要有以下幾種方法:
? ?2.1 引用計(jì)數(shù)法? ??
? ? ? ? 原理很簡(jiǎn)單飘千,就是給每一個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù)器(這里的引用計(jì)數(shù)器只是一個(gè)算法理念堂鲜,具體實(shí)現(xiàn)方案有很多,例如直接在對(duì)象中維護(hù)一個(gè)整型字段护奈,又比如每一個(gè)Class對(duì)象生成實(shí)例時(shí)為其伴隨生成一個(gè)計(jì)數(shù)器對(duì)象以完成更復(fù)雜的工作缔莲,當(dāng)然,計(jì)數(shù)器對(duì)象本身對(duì)原對(duì)象的引用需要特殊處理霉旗,這里暫不討論具體實(shí)現(xiàn)方案痴奏,明白原理即可),初始值為0厌秒,當(dāng)有任何一個(gè)引用指向該對(duì)象時(shí)读拆,引用計(jì)數(shù)器觸發(fā)自增1操作,反之简僧,當(dāng)引用斷開建椰,引用計(jì)數(shù)器觸發(fā)自減1操作,任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就會(huì)被GC視為不會(huì)再被使用可以清理掉的對(duì)象岛马。
????????如上圖所示棉姐,引用計(jì)數(shù)法看似很完美得解決了GC如何判斷對(duì)象是否能回收的問題屠列,但卻有一個(gè)致命缺陷:
? ? ? ? 如上圖所示,引用計(jì)數(shù)的缺陷就是無法解決循環(huán)引用伞矩,理論上笛洛,圖中的Hello和Hi對(duì)象實(shí)例除了彼此引用,不會(huì)再被任何地方用到乃坤,那么就應(yīng)該被GC清理掉苛让,卻因?yàn)橐糜?jì)數(shù)始終為1無法執(zhí)行清理,如果大量這樣的對(duì)象存在就會(huì)導(dǎo)致內(nèi)存溢出湿诊。
????2.2 可達(dá)性分析算法
? ? ? ? 也稱作GC Root搜索法狱杰,這個(gè)算法的基本思想就是通過一系列被稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索引用厅须,搜索所走過的路徑稱為引用鏈仿畸,當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的朗和。
? ? ? ? 光看定義可能有些抽象错沽,下面先用一張圖來表示可達(dá)性分析算法,再來解釋其中包含的一些定義:
? ? ? ? 如圖所示眶拉,在Java虛擬機(jī)中維護(hù)了一個(gè)GC Roots集合千埃,里面裝的是該算法認(rèn)為可以作為GC Roots的對(duì)象,以這些對(duì)象作為起點(diǎn)來檢查引用鏈分析一個(gè)對(duì)象是否可用:
????????對(duì)象Object1被其中一個(gè)GC Roots引用忆植,那么可判斷它為存活狀態(tài)放可,同理可推,被Object1所引用的Object2唱逢,Object3也存活吴侦,被Object3引用的Object4存活,至此坞古,該引用鏈上的所有對(duì)象都被判斷為存活备韧;反之,Object5沒有被任何GC Roots引用痪枫,它就被判斷為不可用對(duì)象织堂,Object6、Object7即使被Object5引用奶陈,但由于Object5不可用易阳,因此Object6、Object7也不可用吃粒,這三個(gè)對(duì)象將會(huì)是被回收的對(duì)象潦俺。
? ? ? ? 理解了可達(dá)性分析算法的原理,那么就剩一個(gè)問題:什么對(duì)象會(huì)被作為GC Roots呢?其實(shí)答案顯而易見事示,正在被使用的和長(zhǎng)期存在的對(duì)象早像,這兩種對(duì)象經(jīng)由上一章的講解也很明了,他們就是存在于方法區(qū)的靜態(tài)變量肖爵、常量以及存在于棧(包括Java虛擬機(jī)棧和本地方法棧)中的局部變量卢鹦。
? ? ? ? 再回到引用計(jì)數(shù)法中的循環(huán)引用問題就迎刃而解了,使用可達(dá)性分析算法代替引用計(jì)數(shù)法后劝堪,存在于棧幀c中的局部變量hi和hello就會(huì)作為GC Roots冀自,他們分別指向堆中的Hi和Hello對(duì)象,此時(shí)的Hello和Hi對(duì)象是存活的秒啦,但當(dāng)方法c執(zhí)行完熬粗,棧幀c被彈出,局部變量hi和hello隨即被回收余境,引用斷開荐糜,那么堆中的Hi和Hello對(duì)象再無其他GC Roots引用,即使他們相互持有對(duì)方的引用葛超,也會(huì)被判斷為可回收的對(duì)象。
? ? 2.3 JVM使用哪種方法判斷對(duì)象是否可回收
? ? ? ? 這個(gè)問題直接上代碼驗(yàn)證:
/**? ? ? ??
* @Author: Lv
* @Date: 2020/11/17 10:35
* @Description: GC 測(cè)試類
*/
public class TestGC {
????private static class Hello{
????????private static final int _1MB=1024*1024;
? ? ? ? //這個(gè)屬性是為了占內(nèi)存延塑,方便查看GC日志時(shí)看清楚是否被回收過
? ? ? ? private byte[] size=new byte[2*_1MB];
? ? ? ? private Hi hi;
? ? ? ? private void setHi(Hi hi){
????????????????this.hi=hi;
? ? ? ? }
}
/**
? ? * 其實(shí)創(chuàng)建兩個(gè)Hello對(duì)象互相引用就可以绣张,這里再寫一個(gè)一模一樣
? ? *?的Hi對(duì)象只是為了讓循環(huán)引用更清晰一點(diǎn)
? ? */
? ? private static class Hi{
????????private static final int _1MB=1024*1024;
? ? ? ? private byte[] size=new byte[2*_1MB];
? ? ? ? private Hello hello;
? ? ? ? private void setHello(Hello hello){
????????????????this.hello=hello;
? ? ? ? }
}
public static void main(String[] args) {
????????Hello hello=new Hello();
? ? ? ? Hi hi=new Hi();
? ? ? ? hello.setHi(hi);
? ? ? ? hi.setHello(hello);
? ? ? ? hello=null;//手動(dòng)斷開連接,與棧幀中的局部變量被回收效果一樣
? ? ? ? hi=null;
? ? ? ? System.gc();//這個(gè)方法只是通知JVM垃圾回收关带,至于什么時(shí)候垃圾回收侥涵,要看JVM自身的調(diào)度
? ? }
}
? ? ? ? 在JVM啟動(dòng)配置項(xiàng)里添加參數(shù):-XX:+PrintGCDetails,用來在控制臺(tái)輸出里查看GC日志宋雏,上面代碼執(zhí)行后完整輸出日志如下:
[GC (System.gc()) [PSYoungGen: 8846K->712K(38400K)] 8846K->720K(125952K), 0.0187288 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
[Full GC (System.gc()) [PSYoungGen: 712K->0K(38400K)] [ParOldGen: 8K->654K(87552K)] 720K->654K(125952K), [Metaspace: 3221K->3221K(1056768K)], 0.0089721 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
????PSYoungGen total 38400K, used 333K [0x00000000d5c00000, 0x00000000d8680000, 0x0000000100000000)
????????eden space 33280K, 1% used [0x00000000d5c00000,0x00000000d5c534a8,0x00000000d7c80000)
????????from space 5120K, 0% used [0x00000000d7c80000,0x00000000d7c80000,0x00000000d8180000)
????????to space 5120K, 0% used [0x00000000d8180000,0x00000000d8180000,0x00000000d8680000)
????ParOldGen total 87552K, used 654K [0x0000000081400000, 0x0000000086980000, 0x00000000d5c00000)
????????object space 87552K, 0% used [0x0000000081400000,0x00000000814a38e8,0x0000000086980000)
????????Metaspace used 3228K, capacity 4500K, committed 4864K, reserved 1056768K
????????class space used 351K, capacity 388K, committed 512K, reserved 1048576K
Process finished with exit code 0
? ? ? ? 這里簡(jiǎn)單梳理一下日志內(nèi)容芜飘,前兩句概述了此次GC的總覽,后面是新生代和老年代(這兩個(gè)分代下面會(huì)講到)各區(qū)域的空間詳細(xì)信息磨总,這里主要講解加粗的頭兩句:
????????開頭的GC和Full GC表示GC的停頓類型(并不是下面要講的分代收集中的觸發(fā)了新生代和老年代垃圾收集嗦明,切勿混淆),如果出現(xiàn)了Full蚪燕,則表示發(fā)生了Stop-The-World(STW)娶牌,意思是執(zhí)行這次GC時(shí),掛起其他所有線程馆纳,只讓GC線程執(zhí)行诗良。
? ? ? ? 緊跟的括號(hào)中System.gc()表示觸發(fā)此次GC的類型,我們這里是手動(dòng)調(diào)用System.gc()觸發(fā)GC鲁驶,所以顯示這個(gè)鉴裹,如果是系統(tǒng)觸發(fā),則不會(huì)有這個(gè)括號(hào)內(nèi)容。
? ??????(以第一句為例)[PSYoungGen径荔,[ParOldGen: 督禽,[Metaspace:這些是發(fā)生GC的區(qū)域名,名稱不固定猖凛,這與具體實(shí)現(xiàn)垃圾回收機(jī)制的垃圾收集器有關(guān)赂蠢,不同的垃圾收集器有不同的命名。這里三個(gè)區(qū)域分別代表新生代辨泳,老年代以及元空間虱岂。
? ?????(以第一句為例)?8846K->712K(38400K)?表示 GC前該區(qū)域已用容量->GC后該區(qū)域已用容量(該區(qū)域總?cè)萘?。
? ? ? ?(以第一句為例)方括號(hào)之外的8846K->720K(125952K) 表示 GC前Java堆已用容量->GC后Java堆已用容量(Java堆總?cè)萘?
? ? ? ?(以第一句為例)最后的?0.0187288 secs 是本次GC的時(shí)長(zhǎng)菠红。至于后面還有個(gè)?[Times: user=0.00 sys=0.00, real=0.02 secs]只有部分垃圾收集器會(huì)給出這樣的更詳細(xì)的時(shí)間第岖,分別是用戶態(tài)消耗的CPU時(shí)間、內(nèi)核態(tài)消耗的CPU時(shí)間以及整個(gè)過程的墻鐘時(shí)間(即不包含非運(yùn)算時(shí)間试溯,例如不包含IO操作耗時(shí))蔑滓,這些概念了解即可,有興趣的可以自查遇绞。
? ? ? ? 了解完日志內(nèi)容含義后键袱,就可以從PSYoungGen: 8846K->712K(38400K)這一句分析出,我們創(chuàng)建的兩個(gè)對(duì)象都在新生代摹闽,且在GC后它們都被回收了蹄咖,這就反證出JVM并未使用引用計(jì)數(shù)法。
? ? ? ? 事實(shí)上付鹿,在主流的商用程序中(Java澜汤,C#),都是使用可達(dá)性分析算法來判定對(duì)象是否存活舵匾。?而引用計(jì)數(shù)法優(yōu)點(diǎn)在于實(shí)現(xiàn)簡(jiǎn)單俊抵,效率也很高,它的使用場(chǎng)景主要在微軟的COM技術(shù)坐梯、使用ActionScript3的FlashPlayer徽诲、Python等等,了解即可吵血。
三馏段、垃圾收集算法
? ? 上面我們已經(jīng)知道JVM采用可達(dá)性分析算法來標(biāo)記一個(gè)對(duì)象是否可以被回收,那么具體如何回收這些被標(biāo)記的對(duì)象呢践瓷,有以下幾種算法(以下都是算法理論院喜,并不是具體實(shí)現(xiàn)):
? ? 3.1 標(biāo)記-清除算法
? ? ? ? 這是最基礎(chǔ)粗暴的一種算法,原理也很簡(jiǎn)單晕翠,當(dāng)堆中的有效內(nèi)存空間被耗盡的時(shí)候喷舀,就會(huì)停止整個(gè)程序砍濒,然后進(jìn)行兩步工作,第一步先把所有的可回收對(duì)象利用上述可達(dá)性分析算法進(jìn)行標(biāo)記硫麻,第二項(xiàng)就是統(tǒng)一回收被標(biāo)記的對(duì)象(這里請(qǐng)注意爸邢,一個(gè)對(duì)象在該過程中只有兩種狀態(tài),一個(gè)是存活拿愧,一個(gè)是可回收杠河,假設(shè)這個(gè)標(biāo)志位為0/1,初始值都為0浇辜,因此這里所說的標(biāo)記可回收對(duì)象的意思可以理解為沿著GC Roots的引用鏈把存活的對(duì)象都標(biāo)記為1券敌,那么相當(dāng)于把可回收對(duì)象標(biāo)記為0),如圖所示:
? ? ? ? 從圖中可以看出該算法存在一個(gè)很直觀的不足之處:在回收之后柳洋,內(nèi)存中會(huì)存在大量的不連續(xù)內(nèi)存碎片待诅,這樣的碎片太多就會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要為大對(duì)象分配空間時(shí),找不到連續(xù)內(nèi)存來存放熊镣,從而提前觸發(fā)下一次垃圾回收卑雁,甚至可能導(dǎo)致內(nèi)存溢出。
? ? ? ? 標(biāo)記清除算法的另一個(gè)不足之處就是它的效率不高绪囱,畢竟經(jīng)歷標(biāo)記和清除兩次遍歷(第一次遍歷標(biāo)記了存活對(duì)象测蹲,遍歷完成后,相當(dāng)于可回收對(duì)象也被標(biāo)記出來鬼吵,第二次遍歷找到這些可回收對(duì)象進(jìn)行清除)弛房,這兩個(gè)過程加起來會(huì)比較費(fèi)時(shí)。
? ? ? ? 但所有的教材都會(huì)把標(biāo)記清除算法放在講解GC算法的第一個(gè)位置來講而柑,就是因?yàn)樗亲罨A(chǔ)的算法,后面的GC算法都是在它基礎(chǔ)上做的優(yōu)化荷逞。
? ? 3.2 復(fù)制算法
? ??????復(fù)制算法將內(nèi)存等分為兩個(gè)區(qū)域媒咳,所有動(dòng)態(tài)分配的對(duì)象都只能分配在其中一個(gè)區(qū)域,而另外一個(gè)區(qū)域始終保持空閑狀態(tài)种远。
? ? ? ? 當(dāng)正在使用的一塊區(qū)域有效內(nèi)存空間耗盡時(shí)涩澡,GC線程會(huì)將該區(qū)域內(nèi)的存活對(duì)象,全部復(fù)制到另一個(gè)空閑區(qū)域坠敷,且嚴(yán)格按照內(nèi)存地址依次排列妙同,然后更新存活對(duì)象的內(nèi)存引用地址指向新的內(nèi)存地址。同時(shí)膝迎,將原區(qū)域中的內(nèi)存(只剩下了可回收對(duì)象)一次性全部回收粥帚,兩個(gè)區(qū)域就完成了互換。
? ? ? ? 很明顯限次,復(fù)制算法效率比標(biāo)記清除算法要高很多芒涡,全部過程都可以在一次遍歷中完成柴灯,而且不會(huì)產(chǎn)生內(nèi)存碎片,新來的對(duì)象分配內(nèi)存也只需要移動(dòng)堆頂指針费尽,按順序存放即可赠群。
? ? ? ? 缺點(diǎn)也很明顯,那就是永遠(yuǎn)都要浪費(fèi)一半的內(nèi)存空間旱幼,假設(shè)極端條件下查描,內(nèi)存中的對(duì)象全部存活且觸發(fā)了GC,那么活著的對(duì)象集體進(jìn)行無用的復(fù)制轉(zhuǎn)移柏卤,原區(qū)域也沒有可回收的對(duì)象進(jìn)行回收冬三,新的對(duì)象還是沒有地方存放,白白浪費(fèi)了一半的空間闷旧,最后造成了內(nèi)存溢出长豁。由這個(gè)極端情況不難考慮出,復(fù)制算法對(duì)于對(duì)象存活率越高時(shí)忙灼,整體空間使用率和效率就會(huì)顯得越低匠襟。
? ? 3.3 標(biāo)記-整理算法
? ? ? ? 為了應(yīng)對(duì)上述復(fù)制算法的缺點(diǎn),標(biāo)記整理算法出現(xiàn)了该园。該算法標(biāo)記出所有存活的對(duì)象酸舍,然后讓所有存活對(duì)象向一端進(jìn)行移動(dòng),最后直接清理存活對(duì)象端邊界以外的內(nèi)存里初。
? ? ? ? 該算法也僅需一次遍歷啃勉,互補(bǔ)于復(fù)制算法,對(duì)象存活率越高時(shí)双妨,效率越高淮阐,試想一下對(duì)象存活率極高時(shí),存活對(duì)象總在內(nèi)存的一端刁品,觸發(fā)GC也不再需要去移動(dòng)它們泣特,反之,如果對(duì)象存活率很低挑随,每一次GC都需要不斷清理可回收對(duì)象状您,又將新的存活對(duì)象標(biāo)記移動(dòng)到存活的一端,那么效率就很低了兜挨。
? ? 3.4 分代收集算法
? ? ? ? 分代收集算法與其說是新的算法膏孟,不如說是上述算法的整合和再優(yōu)化。使用分代收集算法會(huì)把Java堆分為幾個(gè)區(qū)域拌汇,針對(duì)每個(gè)區(qū)域不同的對(duì)象存活情況使用不同的垃圾回收算法柒桑,以提高整體的效率。
? ? ? ? 如圖所示噪舀,該算法總體把Java堆分為兩個(gè)區(qū)域:新生代(Young Generation)和老年代(Old Generation)幕垦,內(nèi)存空間占比1:2丢氢。
? ??????新生代
? ??????幾乎所有新產(chǎn)生的對(duì)象都會(huì)首先進(jìn)入新生代,這里的對(duì)象有一個(gè)特點(diǎn)先改,那就是“朝生夕死”疚察,換句話說就是存活率不高(大量方法的運(yùn)行中創(chuàng)建對(duì)象,方法運(yùn)行完仇奶,局部變量被回收斷開引用)貌嫡,因此這里采用復(fù)制算法,每次只有少量對(duì)象需要被復(fù)制遷移该溯,大量對(duì)象被直接清除岛抄,完美利用復(fù)制算法的機(jī)制,效率很高狈茉。
? ? ? ? 更具體得來說夫椭,新生代被分為伊甸區(qū)(Eden)和生存者區(qū)(Survivor),生存者區(qū)又等分成了From和To兩個(gè)區(qū)域氯庆,三個(gè)分區(qū)的內(nèi)存空間占比為8:1:1(并沒有機(jī)械得使用復(fù)制算法原生定義的兩區(qū)等分蹭秋,而是進(jìn)行了優(yōu)化,因?yàn)樾庐a(chǎn)生的對(duì)象總是很多堤撵,而GC完后存活的對(duì)象卻很少)仁讨,新生成的對(duì)象會(huì)進(jìn)入較大的Eden區(qū)颈畸,當(dāng)Eden區(qū)空間不足會(huì)進(jìn)入其中一個(gè)Survivor區(qū)瘦馍,另一個(gè)Survivor區(qū)就作為復(fù)制算法的空閑區(qū)域,當(dāng)觸發(fā)GC時(shí)茸歧,將Eden和一塊Survivor區(qū)域的所有存活對(duì)象復(fù)制到另一塊Survivor區(qū)域荒给,然后清理到剛存放對(duì)象的區(qū)域丈挟,如此循環(huán)。
????????雖然每次只會(huì)剩少量存活對(duì)象志电,但如果它們始終存在于新生代曙咽,新生代就將無法存放新對(duì)象,這是無法接受的溪北,因此JVM在使用分代收集分代時(shí)對(duì)對(duì)象進(jìn)行了“年齡”標(biāo)記,對(duì)象初始化“年齡”都為0夺脾,每次新生代GC后存活的對(duì)象年齡會(huì)+1之拨,達(dá)到一定數(shù)值后會(huì)被移動(dòng)至老年代(這個(gè)數(shù)值可以配置,通用默認(rèn)值是15咧叭,仍然看具體實(shí)現(xiàn))蚀乔。
? ? ? ? 老年代
老年代存放的對(duì)象都是存活率較高的對(duì)象(例如緩存對(duì)象、數(shù)據(jù)庫連接對(duì)象菲茬、單例對(duì)象等等)吉挣,因此可以采用標(biāo)記-清除或者標(biāo)記-整理算法派撕,具體采用哪種根據(jù)使用的垃圾收集器來進(jìn)行判斷。
? ? ? ? 上面說幾乎所有對(duì)象都會(huì)首先被分配到伊甸區(qū)睬魂,但也有例外终吼,比如特別大的對(duì)象(例如很長(zhǎng)的String或很大的byte[])會(huì)直接進(jìn)入老年代,因?yàn)樗枰艽蟮倪B續(xù)存儲(chǔ)空間氯哮,這個(gè)閾值也可以使用虛擬機(jī)參數(shù)-XX:PretenureSizeThreshold進(jìn)行配置际跪,內(nèi)存大于配置數(shù)的對(duì)象就會(huì)直接進(jìn)入老年代,默認(rèn)值為3MB喉钢。
四姆打、何時(shí)觸發(fā)垃圾回收
? ? ?JVM在進(jìn)行GC時(shí),并非每次都對(duì)所有內(nèi)存區(qū)域一起回收的肠虽,大部分時(shí)候回收的都是指新生代幔戏。因此GC按照回收的區(qū)域又分了兩種類型,一種是普通GC税课,一種是全局GC闲延,它們所針對(duì)的區(qū)域如下:
? ??普通GC(Minor GC):只針對(duì)新生代區(qū)域的GC。
? ??全局GC(Major GC or Full GC):主要針對(duì)老年代的GC伯复,偶爾伴隨對(duì)新生代的GC以及對(duì)永久代的GC(僅JDK1.8以前慨代,現(xiàn)在已經(jīng)由元空間代替永久代實(shí)現(xiàn)方法區(qū),元空間有自己的內(nèi)存管理啸如,并不需要JVM的GC干預(yù))侍匙,由于老年代相對(duì)來說GC效果不好,而且內(nèi)存使用增長(zhǎng)速度也慢叮雳,因此正常情況下想暗,需要經(jīng)過好幾次普通GC,才會(huì)觸發(fā)一次全局GC帘不。
? ??第一種觸發(fā)GC的方式就是虛擬機(jī)會(huì)自行根據(jù)當(dāng)前內(nèi)存大小说莫,判斷何時(shí)進(jìn)行垃圾回收。即當(dāng)新生對(duì)象進(jìn)入伊甸區(qū)無法申請(qǐng)到足夠內(nèi)存時(shí)就會(huì)觸發(fā)普通GC寞焙,而當(dāng)大對(duì)象或新生代里年齡達(dá)到閾值的對(duì)象無法申請(qǐng)到足夠的老年代內(nèi)存時(shí)將會(huì)觸發(fā)全局GC储狭。
? ??第二種是程序員手動(dòng)調(diào)用 System.gc()方法通知JVM進(jìn)行垃圾回收,且這種方式觸發(fā)的是全局GC捣郊,只是它不能保證垃圾回收一定會(huì)進(jìn)行辽狈,具體什么時(shí)候進(jìn)行是取決于具體的虛擬機(jī)的,不同的虛擬機(jī)有不同的對(duì)策呛牲。
五刮萌、總結(jié)
? ? JVM(Sun HotSpot )采用可達(dá)性分析算法判斷一個(gè)對(duì)象是否可回收,并使用分代收集算法進(jìn)行垃圾回收娘扩,只是Java虛擬機(jī)規(guī)范對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)分代收集算法着茸,并沒有任何的規(guī)定壮锻,所以不同的廠商、不同版本的虛擬機(jī)所提供的垃圾收集器都會(huì)有所不同涮阔,并且一般都會(huì)提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點(diǎn)和要求組合出各個(gè)分年區(qū)域所使用的收集器猜绣。
? ? 另外,如果我們的程序頻繁觸發(fā)全局GC澎语,將會(huì)直接影響程序執(zhí)行效率途事,因此在對(duì)JVM調(diào)優(yōu)的過程中,很大一部分工作就是對(duì)于全局GC的調(diào)節(jié),下一章節(jié)我們將一起學(xué)習(xí)JVM調(diào)優(yōu)擅羞。
《淺談Java虛擬機(jī)(一)—什么是Java虛擬機(jī)》
《淺談Java虛擬機(jī)(二)—運(yùn)行時(shí)數(shù)據(jù)區(qū)域》
《淺談Java虛擬機(jī)(四)—JVM調(diào)優(yōu)》
本系列文章參考文檔:《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》--?周志明