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