Android之java的GC垃圾回收機(jī)制詳解-層層分析步步深入
前言
Java語言中一個(gè)顯著的特點(diǎn)就是引入了垃圾回收機(jī)制,使c++程序員最頭疼的內(nèi)存管理的問題迎刃而解,它使得Java程序員在編寫程序的時(shí)候不再需要考慮內(nèi)存管理。
由于有個(gè)垃圾回收機(jī)制,Java中的對(duì)象不再有“作用域”的概念,只有對(duì)象的引用才有“作用域”。垃圾回收可以有效的防止內(nèi)存泄露,有效的使用空閑的內(nèi)存;
垃圾回收機(jī)制大家應(yīng)該都有所了解,它不僅是面試的常客,也是Java體系中相當(dāng)重要的一塊知識(shí)。深入理解Java的GC機(jī)制,不僅有助于我們?cè)陂_發(fā)中提高程序的性能,更有了在面試官面前炫技的資本。
本篇文章將全面且深入的分析JVM的垃圾回收機(jī)制,進(jìn)行講解。
對(duì)象的創(chuàng)建是由JVM完成的,在對(duì)象創(chuàng)建的時(shí)候JVM會(huì)在Java堆中開辟一塊空間用來存儲(chǔ)這個(gè)對(duì)象,而當(dāng)對(duì)象“死亡”的時(shí)候,同樣是由JVM來處理的,JVM處理“死亡”對(duì)象的過程就是垃圾回收機(jī)制。
一、GC機(jī)制
1.堆內(nèi)存的區(qū)域劃分
關(guān)于堆內(nèi)存區(qū)域的劃分,其實(shí)是由垃圾收集器的特性決定的。
為了方便JVM更好的管理和回收對(duì)象,Java的設(shè)計(jì)者們將Java的堆內(nèi)存成為了兩大塊,分別為:
新生代(Young Generation) 和 老年代(Old Generation)
而根據(jù)新生代的特性,又將新生代分成了一塊較大的Eden區(qū)域和兩塊較小但大小相等的Survivor區(qū)域。
至于新時(shí)代和老年代這兩塊區(qū)域,是我們今天要探討的重點(diǎn)。
垃圾回收的特點(diǎn)。垃圾收集器在執(zhí)行一次垃圾回收時(shí),可能是部分收集(Partical GC)也可能是整堆收集(Full GC),部分收集又可以分為新生代收集(Minor GC/Young GC)和老年代收集(Major GC/Old GC)。
既然有這樣的劃分,那收集器回收區(qū)域的規(guī)則是根據(jù)什么條件確定的呢?在JDK6 之后,回收區(qū)域的規(guī)則為:只要老年代的連續(xù)空間大于新生代對(duì)象總大小或者歷次晉升的平均大小,就會(huì)進(jìn)行Minor GC,否則將進(jìn)行Full GC。
對(duì)象通常是在Eden區(qū)域被創(chuàng)建,JVM會(huì)給每個(gè)對(duì)象定義一個(gè)年齡(Age)計(jì)數(shù)器,存儲(chǔ)在對(duì)象頭中。如果經(jīng)過第一次Minor GC后對(duì)象仍然存活,
并且能被Survivor區(qū)域容納的話,對(duì)象則會(huì)被移動(dòng)到Survivor區(qū)域,同時(shí)會(huì)將對(duì)象的年齡設(shè)置為1歲。接下來,該對(duì)象會(huì)經(jīng)歷多次的垃圾回收,
Survivor區(qū)中的對(duì)象每熬過一次Minor GC,它的年齡就會(huì)增加一歲。如果對(duì)這個(gè)象增加到一定年齡(默認(rèn)15,可通過-XX:MaxTenuringThreshold參數(shù)設(shè)置),就會(huì)被移動(dòng)到老年代中。
當(dāng)然,為了更好的適應(yīng)不同程序的內(nèi)存情況,HotSpot虛擬機(jī)并不是絕對(duì)要求對(duì)象年齡達(dá)到后才能轉(zhuǎn)移到老年代,特殊情況有如下兩種:
①如果Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半,則年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代。
②對(duì)于大對(duì)象,HotSpot虛擬機(jī)可通過-XX:PretenureSizeThreshold參數(shù)進(jìn)行設(shè)置,當(dāng)對(duì)象內(nèi)存大于設(shè)定的值的話,這個(gè)對(duì)象會(huì)繞過Eden區(qū)域直接被分配到老年代。
2.永久代(Permanent Generation)
在JDK7以及之前,HotSpot虛擬機(jī)還有另外一塊叫永久代(Permanent Generation) 的存儲(chǔ)區(qū)域,這塊區(qū)域并不屬于堆內(nèi)存,而是對(duì)于方法區(qū)的實(shí)現(xiàn)。
主要用于存放Class和Meta(元數(shù)據(jù))的信息,Class在類加載的時(shí)候被放入永久代。永久代和存放實(shí)例的堆內(nèi)存區(qū)域不同,GC不會(huì)在主程序運(yùn)行期對(duì)永久代進(jìn)行清理。所以這也導(dǎo)致了永久代的區(qū)域會(huì)隨著加載的Class的增多而爆滿,最終拋出OOM異常。雖然被稱為永久代,但這塊內(nèi)存區(qū)域也會(huì)進(jìn)行垃圾回收。
永久代的垃圾收集主要包廢棄常量和無用的類(被類加載器卸載的Class)。永久代觸發(fā)垃圾回收的條件比較困難,需要同時(shí)滿足以下三點(diǎn):
①該類所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實(shí)例;
②加載該類的ClassLoader已經(jīng)被回收;
③該類對(duì)應(yīng)的java.lang.Class 對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法.
3.元空間(MetaSpace)
由于永久代可能存在內(nèi)存溢出的問題,在JDK8之后永久代已經(jīng)不復(fù)存在,取而代之的是元空間(MetaSpace)
元空間的本質(zhì)和永久代類似,不過元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用本地內(nèi)存。
因此,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制,但可以通過-XX:MetaspaceSize這個(gè)參數(shù)來指定初始空間大小,當(dāng)達(dá)到設(shè)置的最大值就會(huì)觸發(fā)垃圾收集進(jìn)行類型卸載,同時(shí)GC會(huì)對(duì)該值進(jìn)行調(diào)整:如果釋放了大量的空間,就適當(dāng)降低該值;如果釋放了很少的空間,那么在不超過MaxMetaspaceSize時(shí),適當(dāng)提高該值??梢酝ㄟ^-XX:MaxMetaspaceSize來設(shè)置元空間能夠使用的最大內(nèi)存,默認(rèn)是沒有限制的。
除了上面兩個(gè)指定大小的選項(xiàng)以外,還有兩個(gè)與 GC 相關(guān)的屬性:-XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空間容量的百分比,減少為分配空間所導(dǎo)致的垃圾收集 -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空間容量的百分比,減少為釋放空間所導(dǎo)致的垃圾收集。
有關(guān)垃圾回收的區(qū)域如下圖所示:
圖中的Permanet Generation區(qū)域,在Jdk8中,被MetaSpace區(qū)取代了
二、垃圾收集的標(biāo)記算法
垃圾收集器回收垃圾的第一步先要確定哪些對(duì)象是可以被回收的。因此,JVM會(huì)掃描堆內(nèi)存中的所有對(duì)象,并標(biāo)記出可被回收的對(duì)象。而垃圾收集的標(biāo)記算法有以下兩種:
1.引用計(jì)數(shù)算法
引用計(jì)數(shù)算法通過在每個(gè)對(duì)象中添加一個(gè)計(jì)數(shù)器,當(dāng)有一個(gè)地方引用它的時(shí)候計(jì)數(shù)器的值就會(huì)增加1;當(dāng)引用失效的時(shí)候計(jì)數(shù)器的值則會(huì)減1。當(dāng)計(jì)數(shù)器的值為0時(shí),則可認(rèn)為這個(gè)對(duì)象已經(jīng)不再使用。因此對(duì)于引用計(jì)數(shù)算法,垃圾收集器只需要回收計(jì)數(shù)器為0的對(duì)象即可。
引用計(jì)數(shù)算法的優(yōu)點(diǎn)是效率很高,不需要遍歷所有對(duì)象。但它是存在一個(gè)致命的缺點(diǎn),即無法解決對(duì)象之間循環(huán)引用的問題。比如對(duì)象A引用了對(duì)象B,對(duì)象B也引用了對(duì)象A,除此之外,A、B兩個(gè)對(duì)象再也沒有被其他地方引用。此時(shí)對(duì)象A和對(duì)象B的計(jì)數(shù)器均不為0,所以A、B兩個(gè)對(duì)象都無法被回收。所以,目前商用的Java虛擬機(jī)都沒有選用引用計(jì)數(shù)算法來進(jìn)行標(biāo)記。
2.可達(dá)性分析算法
可達(dá)性分析算法也被稱為根搜索算法。這一算法的基本思路是用一系列的“GC Roots"的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑被稱為”引用鏈“(Reference Chain)。如果一個(gè)對(duì)象到”GC Roots"沒有任何的引用鏈相連,則證明此對(duì)象可能不再被使用。
如下圖所示,灰色部分的對(duì)象沒有關(guān)聯(lián)到引用鏈上,此時(shí)這些對(duì)象就會(huì)被判定為可回收對(duì)象。
哪些對(duì)象可以被作為GC Roots呢?主要包括以下幾種:
①在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
②方法區(qū)中類靜態(tài)屬性引用的對(duì)象。
③在方法區(qū)中引用的對(duì)象,如字符串常量池(String Table)里的引用
本地方法棧中JNI引用的對(duì)象
④Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對(duì)應(yīng)的Class對(duì)象以及一些常駐的異常對(duì)象等。
⑤所有同步鎖持有的對(duì)象,反應(yīng)Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
三、垃圾收集算法
1.標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記-清除算法是最早出現(xiàn)也是最基礎(chǔ)的一種垃圾收集算法。該算法分為“標(biāo)記”和”清除“兩個(gè)階段,標(biāo)記階段就是上邊講到的對(duì)垃圾的標(biāo)記。首先會(huì)通過可達(dá)性分析算法標(biāo)記出所有需要回收的對(duì)象,然后統(tǒng)一回收掉所有被標(biāo)記的對(duì)象。標(biāo)記-清除算法的執(zhí)行過程如下圖所示:
圖中深灰色區(qū)域?yàn)榭苫厥諈^(qū)域,在標(biāo)記完成后直接將深灰色區(qū)域進(jìn)行清理。這一算法很容易理解,實(shí)現(xiàn)起來也很便捷,但是也存在兩個(gè)缺點(diǎn):
①.執(zhí)行效率會(huì)隨對(duì)象增多而降低。如果Java堆中包含大量需要回收的對(duì)象。此時(shí)需要進(jìn)行大量標(biāo)記和清除操作。導(dǎo)致標(biāo)記和清除這兩個(gè)過程需要大量的時(shí)間,降低了執(zhí)行效率
②引起嚴(yán)重的內(nèi)存碎片化問題。標(biāo)記、清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存空間,這可能會(huì)導(dǎo)致在需要分配大對(duì)象時(shí)無法找到足夠的連續(xù)空間,進(jìn)而引發(fā)GC
2.標(biāo)記-復(fù)制算法(Copying)
標(biāo)記-復(fù)制算法也被簡稱為復(fù)制算法。它是對(duì)標(biāo)記-清除算法的改進(jìn)。復(fù)制算法將內(nèi)存劃分為大小相等的兩塊,分配對(duì)象時(shí)只使用其中的一塊。當(dāng)這塊內(nèi)存用完時(shí),就將存活的對(duì)象復(fù)制到另外一塊上面,然后把已使用的這塊內(nèi)存一次性清理掉。復(fù)制算法的執(zhí)行過程如下圖所示:
復(fù)制算法雖然解決了標(biāo)記-清除算法的一些問題。但其缺陷也顯而易見,直接導(dǎo)致了可用內(nèi)存變?yōu)樵瓉淼囊话耄瑑?nèi)存使用率太低;
3.標(biāo)記-整理算法(Mark-Compact)
標(biāo)記整理算法在標(biāo)記了存活對(duì)象之后,會(huì)讓所有存活的對(duì)象向內(nèi)存的一端移動(dòng),然后直接清除掉邊界外的內(nèi)存。該算法的示意圖如下圖所示:
移動(dòng)存活對(duì)象并更新所有被移動(dòng)對(duì)象的引用是一個(gè)比較耗時(shí)的操作。而且,在移動(dòng)對(duì)象時(shí)必須暫停所有用戶線程才能進(jìn)行(這一操作有個(gè)專有名詞叫“Stop The World”,簡稱STW),拖累了用戶程序的執(zhí)行效率;
4.分代收集(Generational Collection)
分代收集不能稱得上是一種算法,它會(huì)根據(jù)堆內(nèi)存的不同區(qū)域采用不同的收集算法,因地制宜。
比如上邊我們已經(jīng)說過的,在G1收集器之前,所有的收集器都是將Java堆劃分為新生代和老年代,由于新生代中對(duì)象存活率比較低,因此在新時(shí)代采用優(yōu)化了的復(fù)制算法。HotSpot虛擬機(jī)中將Eden和Survivor的大小大小劃分為8:1的比例,分配對(duì)象只使用Eden和其中的一塊Surivor區(qū)域,在標(biāo)記完成后將存活的對(duì)象復(fù)制到另外一塊Survior空間中,然后清除Eden和使用的一塊Surivor。這樣,新生代的空間利用率就達(dá)到了90%。
對(duì)于老年代每次垃圾回收存活的對(duì)象比較多,因此這一區(qū)域采用的是標(biāo)記-整理算法進(jìn)行垃圾回收。
四、垃圾收集器
垃圾收集器其實(shí)就是對(duì)于前面講到的原理的實(shí)現(xiàn),只不過在Java的發(fā)展史中出現(xiàn)了一代又一代的垃圾收集器,而新一代的垃圾收集器都是對(duì)上一代垃圾收集器缺點(diǎn)的彌補(bǔ)。直到前幾天(2020年9月15日),在Oracle JDK15中又引入了新的垃圾收集器Shenandoah??梢娭钡浇裉霬ava的設(shè)計(jì)者們依然還在對(duì)收集器進(jìn)行優(yōu)化。
經(jīng)典的幾款垃圾收集器,圖中連線表示這兩款收集器可以配合使用
1.新生代收集器
①Serial收集器
Serial收集器是最基礎(chǔ)、發(fā)展歷史最悠久的收集器。它是一個(gè)單線程工作的收集器,
對(duì)于早期的單核處理器或處理器核心數(shù)較少的情況下,Serial收集器由于沒有線程交互的開銷,
所以收集效率比較高。但是,Serial收集器整個(gè)收集過程是需要STW的。這也是導(dǎo)致了早期的Java程序慢的主要原因之一。Serial收集器新生代采用的是標(biāo)記-復(fù)制算法,運(yùn)行過程如下圖所示
②Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標(biāo)記-復(fù)制算法實(shí)現(xiàn)。也是能夠并行收集的多線程收集器,從表面上看它與ParNew非常相似,Parallel Scavenge收集器的目標(biāo)是達(dá)到一個(gè)可控制的吞吐量(Throughput);
吞吐量=(運(yùn)行用戶代碼的時(shí)間)/(運(yùn)行用戶代碼時(shí)間+運(yùn)行垃圾收集時(shí)間)
Parallel Scavenge收集器運(yùn)行過程如下圖所示:
2.老年代收集器
① Serial Old收集器
Serial Old是Serial收集器的老年代版本,它與Serial一樣都是單線程收集器。Serial Old使用的是標(biāo)記-整理算法。它的主要意義也是提供客戶端模式下的HotSpot虛擬機(jī)使用。
② Parallel Old收集器
Parallel Old 是Parallel Scavenge收集器的老年代版本,支持多線程并發(fā)收集,基于標(biāo)記-整理算法。這個(gè)收集器是在JDK 6時(shí)開始提供。
③ CMS收集器
CMS(Concurrent Mark Sweep)收集器是一款具有劃時(shí)代意義的收集器。前面我們提到的幾款收集器在工作期間全程都需要STW,而CMS第一次實(shí)現(xiàn)了垃圾收集的并發(fā)處理。因此,這款收集器可以有效的減少垃圾收集過程中的停頓時(shí)間。CMS收集器是基于標(biāo)記-清除算法實(shí)現(xiàn)的。我們來詳細(xì)了解一下CMS的工作過程:
(1)初始標(biāo)記:從GC Roots出發(fā)標(biāo)記全部直接子節(jié)點(diǎn)的過程,該階段是STW的。由于GC Roots數(shù)量不多,通常該階段耗時(shí)非常短。
(2)并發(fā)標(biāo)記:并發(fā)標(biāo)記階段是指從GC Roots開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,找出存活對(duì)象。該階段是并發(fā)的,即應(yīng)用線程和GC線程可以同時(shí)活動(dòng)。并發(fā)標(biāo)記耗時(shí)相對(duì)長很多,但因?yàn)椴皇荢TW,所以我們不太關(guān)心該階段耗時(shí)的長短。
(3)重新標(biāo)記:重新標(biāo)記那些在并發(fā)標(biāo)記階段發(fā)生變化的對(duì)象。該階段是STW的。
(4)并非清除:并行清理, 開啟用戶線程,同時(shí)GC線程開始對(duì)為標(biāo)記的區(qū)域做清掃。
從上面描述可以看到,CMS能夠并發(fā)收集,有效減少停頓時(shí)間。但CMS并不是一款完美的垃圾收集器,不然也不會(huì)在JDK15中將其移除。它的缺點(diǎn)主要有以下幾個(gè):
(1)并發(fā)收集占用CPU資源。雖然并發(fā)階段不會(huì)導(dǎo)致用戶停頓,并發(fā)時(shí)的收集線程卻占用了一部分CPU資源,導(dǎo)致應(yīng)用程序變慢,降低了吞吐量。
(2)無法處理浮動(dòng)垃圾。CMS的并發(fā)標(biāo)記和并發(fā)清理階段,用戶線程是繼續(xù)運(yùn)行的,這期間必然會(huì)有新的垃圾對(duì)象產(chǎn)生。對(duì)于已經(jīng)收集過的區(qū)域,CMS無法再去回頭處理它們,只能等到下一次垃圾收集時(shí)再清理掉。
(3)并發(fā)清理階段需要保證內(nèi)存充足。由于在垃圾收集階段用戶線程依然在運(yùn)行,所有不得不預(yù)留足夠的空間提供給用戶線程使用。因此CMS收集器在垃圾收集開始時(shí)需要預(yù)留足夠的內(nèi)存。JDK 5的默認(rèn)設(shè)置,當(dāng)老年代使用了68%的空間后就垃圾收集會(huì)被激活。雖然可以通過參數(shù)-XX:CMSInitiatingOccupancyFraction來調(diào)高CMS的觸發(fā)百分比,但這樣又會(huì)導(dǎo)致CMS運(yùn)行期間可能出現(xiàn)預(yù)留內(nèi)存不足的情況。此時(shí),CMS就會(huì)出現(xiàn)一次”并發(fā)失敗“(Concurrent Mode Failure),虛擬機(jī)不得不啟動(dòng)后備預(yù)案,停止用戶線程的執(zhí)行,啟動(dòng)Serial Old收集器重新進(jìn)行老年代的垃圾收集。
(4)產(chǎn)生大量碎片空間 。由于CMS使用的是“標(biāo)記-清除”算法,因此會(huì)導(dǎo)致大量空間碎片產(chǎn)生。
總結(jié):
這篇文章從堆的分代到垃圾收集算法再到垃圾收集器都做了比較詳細(xì)的分解,一步一步分析,為了讓老鐵們多學(xué)習(xí)下gc這方面的知識(shí)點(diǎn)。