Java自動內(nèi)存管理機制學(xué)習(xí):垃圾回收器與內(nèi)存分配策略

發(fā)表于 討論求助 2023-05-10 14:56:27

備注本文引自《深入理解Java虛擬機第二版》僅供參考

垃圾收集器與內(nèi)存分配策略

概述

GC要完成3件事:

  1. 哪些內(nèi)存需要回收?

  2. 什么時候回收?

  3. 如何回收?

Java內(nèi)存運行時區(qū)域的各部分,其中程序計數(shù)器、虛擬機棧、本地方法棧3個區(qū)域隨線程而生,隨線程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著入棧和出棧操作。每一個棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時就已知的,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個區(qū)域內(nèi)就不需要過多考慮回收的問題,因為方法結(jié)束或者線程結(jié)束,內(nèi)存自然就跟隨著回收了。

而Java堆和方法區(qū)則不一樣,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣,只有在程序處于運行期間時才能知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收是動態(tài)的,垃圾收集器所關(guān)注的是這部分的內(nèi)存。

對象已死嗎

引用計數(shù)算法

給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它的地方,計數(shù)器值+1;當(dāng)引用失效,計數(shù)器值就減1;任何時候計數(shù)器為0,對象就不可能再被引用了。

它很難解決對象之間相互循環(huán)引用的問題。

可達(dá)性分析算法

在主流的商用語言(Java、C#)中都使用可達(dá)性分析(Reachability Analysis)來判定對象是否存活的。

通過一系列的稱為“GC Roots”的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。

object5 6 7 對于GC Roots是不可達(dá)的,所以會被判定為回收對象

在Java語言中,可以作為Gc Roots的對象包括下面幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。

  2. 方法區(qū)中類靜態(tài)屬性引用的對象。

  3. 方法區(qū)中常量引用的對象。

  4. 本地方法棧中JNI(即一般說的Native方法)引用的對象。

再談引用

在JDK 1.2 之后,Java對引用的概念進(jìn)行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

  1. 強引用就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象。

  2. 軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類來實現(xiàn)軟引用。

  3. 弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。在JDK 1.2之后,提供了WeakReference類來實現(xiàn)弱引用。

  4. 虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2之后,提供了PhantomReference類來實現(xiàn)虛引用。

生存還是死亡?

即使在可達(dá)性分析算法中不可達(dá)的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當(dāng)對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機調(diào)用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。

如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會放置在一個叫做F-Queue的隊列之中,并在稍后由一個由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里所謂的“執(zhí)行”是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束,這樣做的原因是,如果一個對象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán)(更極端的情況),將很可能會導(dǎo)致F-Queue隊列中其他對象永久處于等待,甚至導(dǎo)致整個內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GC將對F-Queue中的對象進(jìn)行第二次小規(guī)模的標(biāo)記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,譬如把自己(this關(guān)鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標(biāo)記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。下面例子可以看出finalize()被執(zhí)行,但是它仍然可以存活。

package cc.wsyw126.java.garbageCollection;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 method executed!");
 ? ? ? ?SAVE_HOOK = this;
 ? ?} ? ?public static void main(String[] args) throws InterruptedException {
 ? ? ? ?SAVE_HOOK = new FinalizeEscapeGC();

 ? ? ? ?SAVE_HOOK = null;
 ? ? ? ?System.gc(); ? ? ? ?//因為finalize方法優(yōu)先級很低,所以暫停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(); ? ? ? ?//因為finalize方法優(yōu)先級很低,所以暫停0.5秒等待它
 ? ? ? ?Thread.sleep(500); ? ? ? ?if (SAVE_HOOK != null) {
 ? ? ? ? ? ?SAVE_HOOK.isAlive();
 ? ? ? ?} else {
 ? ? ? ? ? ?System.out.println("no, i am dead :(");
 ? ? ? ?}
 ? ?}
}

?

運行結(jié)果:

finalize method executed!yes, I am still alive :)
no, i am dead :(

?

一樣的代碼,一次逃脫,一次失敗。因為對象的finalize()只能被系統(tǒng)執(zhí)行一次。

建議大家盡量避免使用它,因為它不是C/C++中的析構(gòu)函數(shù),而是Java剛誕生時為了使C/C++程序員更容易接受它所做出的一個妥協(xié)。它的運行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序。

回收方法區(qū)

在堆中,尤其是在新生代中,常規(guī)應(yīng)用進(jìn)行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠(yuǎn)低于此。

永久代的垃圾回收主要回收兩部分內(nèi)容:廢棄常量和無用的類?!皬U棄常量”判斷比較簡單,但是“無用的類”的判斷復(fù)雜一些,需要滿足下面3個條件:

  1. 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例。

  2. 加載該類的ClassLoader已經(jīng)被回收。

  3. 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

是否對類進(jìn)行回收,HotSpot虛擬機提供了-Xnoclassgc參數(shù)進(jìn)行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading, -XX:+TraceClassUnLoading查看類架子啊和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機支持。

在大量使用反射、動態(tài)代理、Cglib等ByteCode框架、動態(tài)生成JSP以及OSGI這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

垃圾收集算法

標(biāo)記-清除算法

如同它的名字一樣,算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。它的主要不足有兩個:一個是效率問題,標(biāo)記和清除兩個過程的效率都不高;另一個是空間問題,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。標(biāo)記—清除算法的執(zhí)行過程如下圖所示:

復(fù)制算法

為了解決效率問題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。只是這種算法的代價是將內(nèi)存縮小為了原來的一半,未免太高了一點。復(fù)制算法的執(zhí)行過程如下圖所示:

現(xiàn)在的商業(yè)虛擬機都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當(dāng)回收時,將Eden和Survivor中還存活著的對象一次性地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認(rèn)Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%(80%+10%),只有10%的內(nèi)存會被“浪費”。當(dāng)然,98%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活,當(dāng)Survivor空間不夠用時,需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)

分配擔(dān)保:如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔(dān)保機制進(jìn)入老年代。

標(biāo)記-整理算法

標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存,“標(biāo)記-整理”算法的示意圖如圖所示。

分代收集算法

根據(jù)對象存活周期的不同將內(nèi)存分為幾塊。一般把Java堆分為新生代和老年代,根據(jù)各個年代的特點采用最合適的收集算法。在新生代中,每次垃圾收集時有大批對象死去,只有少量存活,可以選用復(fù)制算法。而老年代對象存活率高,使用標(biāo)記清除或者標(biāo)記整理算法。

HotSpot的算法實現(xiàn)

枚舉根節(jié)點

從可達(dá)性分析中從GC Roots節(jié)點找引用鏈這個操作為例,可作為GC Roots的節(jié)點主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如幀棧中的本地變量表)中,現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百兆,如果要逐個檢查這里面的引用,那么必然會消耗很多時間。

另外,可達(dá)性分析對執(zhí)行時間的敏感還體現(xiàn)在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進(jìn)行–這里“一致性”的意思是指在整個分析期間整個執(zhí)行系統(tǒng)看起來就像被凍結(jié)在某個時間點上,不可以出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化的情況,該點不滿足的話分析結(jié)果準(zhǔn)確性就無法得到保證。這點是導(dǎo)致GC進(jìn)行時必須停頓所有Java執(zhí)行線程(Sun將這件事情稱為“Stop The World”)的其中一個重要原因,即使是在號稱(幾乎)不會發(fā)生停頓的CMS收集器中,枚舉根節(jié)點也是必須要停頓的。

由于目前的主流Java虛擬機使用的都是準(zhǔn)確式GC,所以當(dāng)執(zhí)行系統(tǒng)停頓下來后,并不需要一個不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機應(yīng)當(dāng)是有辦法直接得知哪些地方存放著對象的引用。在HotSpot的實現(xiàn)中,是使用一組稱為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個目的的,在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。

安全點

在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確地完成GC Roots枚舉,但一個很現(xiàn)實的問題隨之而來:可能導(dǎo)致引用關(guān)系變化,或者說OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對應(yīng)的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。

實際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經(jīng)提到,只是在“特定的位置”記錄了這些信息,這些位置稱為安全點(Safepoint),即程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有在到達(dá)安全點時才能暫停。Safepoint的選定即不能太少以至于讓GC等待時間太長,也不能過于頻繁以至于過分增大運行時負(fù)荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的–因為每條指令執(zhí)行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,所以具有這些功能的指令才會產(chǎn)生Safepoint。

對于Safepoint,另一個需要考慮的問題是如何在GC發(fā)生時讓所以線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供選擇:

  1. 搶先式中斷(Preemptive Suspension)

  2. 主動式中斷(Voluntary Suspension)

其中搶先式中斷不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復(fù)線程,讓它“跑”到安全點上?,F(xiàn)在幾乎沒有虛擬機實現(xiàn)采用搶先式中斷來暫停線程從而響應(yīng)GC事件。

而主動式中斷的思想是當(dāng)GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設(shè)置一個標(biāo)志,各個線程執(zhí)行時主動去輪詢這個標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時就自己中斷掛起。輪詢標(biāo)志的地方和安全點是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方。

安全區(qū)域

使用Safepoint似乎已經(jīng)完美地解決了如何進(jìn)入GC的問題,但實際情況卻并不一定。Safepoint機制保證了程序執(zhí)行時,在不太長的時間內(nèi)就會遇到可進(jìn)入GC的Safepoint。但是,程序就”不執(zhí)行“的時候呢?所謂的程序不執(zhí)行就是沒有分配CPU時間,典型的例子就是線程處于Sleep狀態(tài)或者Blocked狀態(tài),這時候線程無法響應(yīng)JVM的中斷請求,”走“到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。對于這種情況,就需要安全區(qū)域(Safe Region)來解決。

安全區(qū)域是指在一段代碼片段之中,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴展了的Safepoint。

在線程執(zhí)行到Safe Region中的代碼時,首先標(biāo)識自己已經(jīng)進(jìn)入了Safe Region,那樣,當(dāng)在這段時間里JVM要發(fā)起GC時,就不用管標(biāo)識自己為Safe Region狀態(tài)的線程了。在線程要離開Safe Region時,它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點枚舉(或者是整個GC過程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須繼續(xù)等待直到收到可以安全離開Safe Region的信號為止。

垃圾收集器

Serial Collecor

Serial收集器是單線程收集器,是分代收集器。它進(jìn)行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結(jié)束。

  1. 新生代:單線程復(fù)制收集算法;

  2. 老年代:單線程標(biāo)記整理算法。

Serial一般在單核的機器上使用,是Java 5非服務(wù)端JVM的默認(rèn)收集器,參數(shù)-XX:UseSerialGC設(shè)置使用。

ParNew收集器

ParNew收集器和Serial收集器的主要區(qū)別是

  1. 新生代的收集,一個是單線程一個是多線程。

  2. 老年代的收集和Serial收集器是一樣的。

實際上是Serial收集器的多線程版本,擁有可控制參數(shù)(如:-XX:SurvivorRatio, -XX:PretenureSizeThreshold, -XX:HandlePromotionFailure等),收集算法,停頓,對象分配規(guī)則,回收策略都和Serial收集器完全一樣。

ParNew收集器是許多運行在server模式下的虛擬機中首選的新生代收集器,一個重要的原因是,只有ParNew和Serial收集器能和CMS收集器共同工作。無法與JDK1.4中存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew和Serial。

ParNew收集器是使用-XX:+UseConcMarkSweepGC選項的默認(rèn)新生代收集器。也可以用-XX:+UseParNewGC選項來強制指定它。

ParNew收集器在單CPU環(huán)境中不比Serial效果好,甚至可能更差,兩個CPU也不一定跑的過,但隨著CPU數(shù)量的增加,性能會逐步增加。默認(rèn)開啟的收集線程數(shù)與CPU數(shù)量相同。在CPU數(shù)量很多的情況下,可以使用-XX:ParallelGCThreads參數(shù)來限制線程數(shù)。

Parallel并行: 多垃圾收集線程并行工作,但用戶線程仍需等待

Concurrent并發(fā):用戶線程和垃圾收集同時進(jìn)行。

Parallel Scavenge收集器

同ParNew一樣是使用復(fù)制算法的新生代并行多線程收集器。

Parallel Scavenge的特點是它的關(guān)注點與其他收集器不同,CMS等收集器的關(guān)注點盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個可控制的吞吐量(Throughput)。所謂吞吐量就是CPU用于運行用戶代碼與CPU總消耗時間的比值。

高吞吐量和停頓時間短的策略相比,主要強調(diào)任務(wù)更快完成,而后者強調(diào)用戶交互體驗。

Parallel Scavenge提供兩個參數(shù)控制垃圾回收停頓時間:-XX:MaxGCPauseMillis和-XX:GCTimeRatio

  1. MaxGCPauseMillis允許的值是一個大于零的毫秒數(shù),收集器將盡力保證內(nèi)存回收話費的時間不超過設(shè)定值。GC停頓時間縮小是以犧牲吞吐量和新生代空間來換取的,也就是要使停頓時間更短,垃圾回收的頻率會增加。

  2. GCTimeRatio的值是一個大于0小于100的整數(shù),也就是垃圾收集時間占總時間的比率。設(shè)為19,則允許最大GC時間就占總時間的5%(1/(1+19)),默認(rèn)99.

Parallel Scavenge收集器也被稱為吞吐量優(yōu)先收集器。

還有一個參數(shù), -XX:+UseAdaptiveSizePolicy,是個開關(guān)參數(shù),打開后會自動調(diào)整Eden/Survivor比例,老年代對象年齡,新生代大小等。這個參數(shù)也是Parallel Scavenge和ParNew的重要區(qū)別。

Serial Old收集器

是Serial的老年代版本,同樣是單線程收集器,使用標(biāo)記-整理算法。主要是client模式下的虛擬機使用

兩大用途:

  1. 在JDK1.5及之前的版本中與Parallel Scavenge搭配使用。

  2. 作為CMS收集器的后備預(yù)案。在并發(fā)收集發(fā)生Concurrent Mode Failure時使用。

Parallel Old收集器

是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記-整理算法。在JDK1.6中才開始使用。由于之前的版本中,Parallel Scavenge只有使用Serial Old作為老年代收集器,其吞吐量優(yōu)先的設(shè)計思路不能被很好的貫徹,在Parallel Old收集器出現(xiàn)后,這兩者的配合主要用于貫徹這種思路。

CMS收集器

Concurrent Mark Sweep 以獲取最短回收停頓時間為目標(biāo)的收集器,比較理想的應(yīng)用場景是B/S架構(gòu)的服務(wù)器。

基于標(biāo)記-清除算法實現(xiàn),運行過程分成4個步驟:

  1. 初始標(biāo)記(需要stop the world),標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快。

  2. 并發(fā)標(biāo)記,進(jìn)行GC Roots Tracing的過程。

  3. 重新標(biāo)記(需要stop the world),為了修正并發(fā)標(biāo)記時用戶繼續(xù)運行而產(chǎn)生的標(biāo)記變化,停頓時間比初始標(biāo)記長,遠(yuǎn)比并發(fā)標(biāo)記短。

  4. 并發(fā)清除

缺點:

  1. CMS收集器對CPU資源非常敏感。在并發(fā)階段,它雖然不會導(dǎo)致用戶線程停頓,但是因為占用了一部分CPU資源而導(dǎo)致應(yīng)用程序變慢,總吞吐量就會降低。CMS默認(rèn)啟動的回收線程數(shù)為(CPU數(shù)量+3)/4。為了解決這一情況,有一個變種i-CMS,但目前并不推薦使用。

  2. CMS收集器無法處理浮動垃圾(floating garbage).可能會出現(xiàn)concurrent mode failure導(dǎo)致另一次full gc的產(chǎn)生。在CMS的并發(fā)清理階段,由于程序還在運行,垃圾還會不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS無法在本次收集中處理掉它們,只好留到下一次GC再處理。這種垃圾稱為浮動垃圾。同樣由于CMS GC階段用戶線程還需要運行,即還需要預(yù)留足夠的內(nèi)存空間供用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被灌滿了再進(jìn)行收集而需要預(yù)留一部分空間提供并發(fā)收集時的程序運作使用。默認(rèn)設(shè)置下,CMS收集器在老年代使用了68%的空間后就會被激活。這個值可以用-XX:CMSInitiatingOccupancyFraction來設(shè)置。要是CMS運行期間預(yù)留的內(nèi)存無法滿足程序需要,就會出現(xiàn)concurrent mode failure,這時候就會啟用Serial Old收集器作為備用進(jìn)行老年代的垃圾收集。

  3. 空間碎片過多(標(biāo)記-清除算法的弊端),提供-XX:+UseCMSCompactAtFullCollection參數(shù),應(yīng)用于在FULL GC后再進(jìn)行一個碎片整理過程。-XX:CMSFullGCsBeforeCompaction,多少次不壓縮的full gc后來一次帶壓縮的。

G1收集器

G1. Garbage first,尚在研發(fā)階段,使用標(biāo)記-整理算法,精確控制停頓,極力避免全區(qū)域垃圾收集。前面的收集器進(jìn)行的收集范圍都是整個新生代或老年代,而G1將整個JAVA堆劃分為多個大小固定的獨立區(qū)域,跟蹤這些區(qū)域里面的垃圾堆積程度,在后臺維護一個優(yōu)先列表,每次在允許的收集時間里,優(yōu)先回收垃圾最多的區(qū)域。

理解GC日志

[GC [PSYoungGen: 8987K->1016K(9216K)] 9984K->5056K(19456K), 0.0569611 secs] [Times: user=0.03 sys=0.02, real=0.06 secs] 
[GC [PSYoungGen: 8038K->1000K(9216K)] 12078K->10425K(19456K), 0.0709523 secs] [Times: user=0.05 sys=0.00, real=0.07 secs] 
[Full GC [PSYoungGen: 1000K->0K(9216K)] [ParOldGen: 9425K->8418K(10240K)] 10425K->8418K(19456K) [PSPermGen: 9678K->9675K(21504K)], 0.3152834 secs] [Times: user=0.39 sys=0.00, real=0.32 secs] 
[Full GC [PSYoungGen: 8192K->3583K(9216K)] [ParOldGen: 8418K->9508K(10240K)] 16610K->13092K(19456K) [PSPermGen: 9675K->9675K(22016K)], 0.1913859 secs] [Times: user=0.34 sys=0.00, real=0.19 secs] 
[Full GC [PSYoungGen: 7716K->7702K(9216K)] [ParOldGen: 9508K->9508K(10240K)] 17224K->17210K(19456K) [PSPermGen: 9675K->9675K(21504K)], 0.2769775 secs] [Times: user=0.52 sys=0.00, real=0.28 secs] 
[Full GC [PSYoungGen: 7702K->7702K(9216K)] [ParOldGen: 9508K->9409K(10240K)] 17210K->17111K(19456K) [PSPermGen: 9675K->9675K(21504K)], 0.2491993 secs] [Times: user=0.64 sys=0.00, real=0.25 secs]
  1. “[GC”和“[full DC”說明了這次垃圾回收的停頓類型。如果是調(diào)用System.gc()方法所觸發(fā)的收集,那么這里顯示“[Full DC(System)”.

  2. [DefNew、[Tenured、[Perm?表示GC發(fā)生的區(qū)域。如果是ParNew收集器,新生代名為“[ParNew”.如果采用Parallel Scavenge收集器,那它配套的新生代名為”[PSYoungGen”。對于老年代和永久代同理。

  3. [PSYoungGen: 8987K->1016K(9216K)] 9984K->5056K(19456K), 0.0569611 secs]中后面的數(shù)字含義是:GC前該內(nèi)存區(qū)域已使用容量->GC后該內(nèi)存區(qū)域已使用容量(該區(qū)域總?cè)萘浚?。而方括號之外的表示“GC前Java堆已經(jīng)使用的容量 -> GC后Java堆已經(jīng)使用的容量(Java堆總?cè)萘浚?。后面的時間是該區(qū)域GC所占用的時間,單位是秒。

  4. [Times: user=0.03 sys=0.02, real=0.06 secs]這里的user、sys和real與Linux的time命令所輸出的時間含義一,分別代表用戶態(tài)消耗的CPU時間,內(nèi)核態(tài)消耗的CPU時間和操作從開始到結(jié)束所經(jīng)過的墻鐘時間。

垃圾收集器參數(shù)總結(jié)

參  數(shù)描  述
UseSerialGC虛擬機運行在Client模式下的默認(rèn)值,打開此開關(guān)后,使用Serial + Serial Old的收集器組合進(jìn)行內(nèi)存回收
UseParNewGC打開此開關(guān)后,使用ParNew + Serial Old的收集器組合進(jìn)行內(nèi)存回收
UseConcMarkSweepGC打開此開關(guān)后,使用ParNew + CMS + Serial Old的收集器組合進(jìn)行內(nèi)存回收。Serial Old收集器將作為CMS收集器出現(xiàn)Concurrent Mode Failure失敗后的后備收集器使用
UseParallelGC虛擬機運行在Server模式下的默認(rèn)值,打開此開關(guān)后,使用Parallel Scavenge + Serial Old(PS MarkSweep)的收集器組合進(jìn)行內(nèi)存回收
UseParallelOldGC打開此開關(guān)后,使用Parallel Scavenge + Parallel Old的收集器組合進(jìn)行內(nèi)存回收
SurvivorRatio新生代中Eden區(qū)域與Survivor區(qū)域的容量比值,默認(rèn)為8,代表Eden∶Survivor=8∶1
PretenureSizeThreshold直接晉升到老年代的對象大小,設(shè)置這個參數(shù)后,大于這個參數(shù)的對象將直接在老年代分配
MaxTenuringThreshold晉升到老年代的對象年齡。每個對象在堅持過一次Minor GC之后,年齡就增加1,當(dāng)超過這個參數(shù)值時就進(jìn)入老年代
UseAdaptiveSizePolicy動態(tài)調(diào)整Java堆中各個區(qū)域的大小以及進(jìn)入老年代的年齡
HandlePromotionFailure是否允許分配擔(dān)保失敗,即老年代的剩余空間不足以應(yīng)付新生代的整個Eden和Survivor區(qū)的所有對象都存活的極端情況
ParallelGCThreads設(shè)置并行GC時進(jìn)行內(nèi)存回收的線程數(shù)
GCTimeRatioGC時間占總時間的比率,默認(rèn)值為99,即允許1%的GC時間。僅在使用Parallel Scavenge收集器時生效
MaxGCPauseMillis設(shè)置GC的最大停頓時間。僅在使用Parallel Scavenge收集器時生效
CMSInitiatingOccupancyFraction設(shè)置CMS收集器在老年代空間被使用多少后觸發(fā)垃圾收集。默認(rèn)值為68%,僅在使用CMS收集器時生效
UseCMSCompactAtFullCollection設(shè)置CMS收集器在完成垃圾收集后是否要進(jìn)行一次內(nèi)存碎片整理。僅在使用CMS收集器時生效
CMSFullGCsBeforeCompaction設(shè)置CMS收集器在進(jìn)行若干次垃圾收集后再啟動一次內(nèi)存碎片整理。僅在使用CMS收集器時生效

內(nèi)存分配與回收策略

對象的內(nèi)存分配,往大方向講,就是在堆上分配(但也可能經(jīng)過JIT編譯后被拆散為標(biāo)量類型并間接地棧上分配),對象主要分配在新生代的Eden區(qū)上,如果啟動了本地線程分配緩沖,將按線程優(yōu)先在TLAB上分配。少數(shù)情況下也可能會直接分配在老年代中,分配的規(guī)則并不是百分之百固定的,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集器組合,還有虛擬機中與內(nèi)存相關(guān)的參數(shù)的設(shè)置。

接下來我們將會講解幾條最普遍的內(nèi)存分配規(guī)則,并通過代碼去驗證這些規(guī)則。本節(jié)下面的代碼在測試時使用Client模式虛擬機運行,沒有手工指定收集器組合,換句話說,驗證的是在使用Serial / Serial Old收集器下(ParNew / Serial Old收集器組合的規(guī)則也基本一致)的內(nèi)存分配和回收的策略。讀者不妨根據(jù)自己項目中使用的收集器寫一些程序去驗證一下使用其他幾種收集器的內(nèi)存分配策略。

對象優(yōu)先在Eden分配

大多數(shù)情況下,對象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時,虛擬機將發(fā)起一次Minor GC。

虛擬機提供了-XX:+PrintGCDetails這個收集器日志參數(shù),告訴虛擬機在發(fā)生垃圾收集行為時打印內(nèi)存回收日志,并且在進(jìn)程退出的時候輸出當(dāng)前的內(nèi)存各區(qū)域分配情況。在實際應(yīng)用中,內(nèi)存回收日志一般是打印到文件后通過日志工具進(jìn)行分析,不過本實驗的日志并不多,直接閱讀就能看得很清楚。

下面代碼的testAllocation()方法中,嘗試分配3個2MB大小和1個4MB大小的對象,在運行時通過-Xms20M、 -Xmx20M、 -Xmn10M這3個參數(shù)限制了Java堆大小為20MB,不可擴展,其中10MB分配給新生代,剩下的10MB分配給老年代。-XX:SurvivorRatio=8決定了新生代中Eden區(qū)與一個Survivor區(qū)的空間比例是8∶1,從輸出的結(jié)果也可以清晰地看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代總可用空間為9216KB(Eden區(qū)+1個Survivor區(qū)的總?cè)萘浚?/p>

執(zhí)行testAllocation()中分配allocation4對象的語句時會發(fā)生一次Minor GC,這次GC的結(jié)果是新生代6651KB變?yōu)?48KB,而總內(nèi)存占用量則幾乎沒有減少(因為allocation1、allocation2、allocation3三個對象都是存活的,虛擬機幾乎沒有找到可回收的對象)。這次GC發(fā)生的原因是給allocation4分配內(nèi)存的時候,發(fā)現(xiàn)Eden已經(jīng)被占用了6MB,剩余空間已不足以分配allocation4所需的4MB內(nèi)存,因此發(fā)生Minor GC。GC期間虛擬機又發(fā)現(xiàn)已有的3個2MB大小的對象全部無法放入Survivor空間(Survivor空間只有1MB大?。灾缓猛ㄟ^分配擔(dān)保機制提前轉(zhuǎn)移到老年代去。

這次GC結(jié)束后,4MB的allocation4對象順利分配在Eden中,因此程序執(zhí)行完的結(jié)果是Eden占用4MB(被allocation4占用),Survivor空閑,老年代被占用6MB(被allocation1、allocation2、allocation3占用)。通過GC日志可以證實這一點。

注意:

新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動作,因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。

老年代GC(Major GC / Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC,經(jīng)常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程)。Major GC的速度一般會比Minor GC慢10倍以上。

private static final int _1MB = 1024 * 1024;/**
 * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 ?*/public static void testAllocation() { ? byte[] allocation1, allocation2, allocation3, allocation4;
 ? allocation1 = new byte[2 * _1MB];
 ? allocation2 = new byte[2 * _1MB];
 ? allocation3 = new byte[2 * _1MB];
 ? allocation4 = new byte[4 * _1MB]; ?// 出現(xiàn)一次Minor GC
 }

?

運行結(jié)果:

[GC [DefNew: 6651K->148K(9216K), 0.0070106 secs] 6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation ? total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
 ?eden space 8192K, ?51% used [0x029d0000, 0x02de4828, 0x031d0000)
 ?from space 1024K, ?14% used [0x032d0000, 0x032f5370, 0x033d0000)
 ?to ? space 1024K, ? 0% used [0x031d0000, 0x031d0000, 0x032d0000)
 tenured generation ? total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)
 ? the space 10240K, ?60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)
 compacting perm gen ?total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
 ? the space 12288K, ?17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

大對象直接進(jìn)入老年代

所謂的大對象是指,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組(筆者列出的例子中的byte[]數(shù)組就是典型的大對象)。大對象對虛擬機的內(nèi)存分配來說就是一個壞消息(替Java虛擬機抱怨一句,比遇到一個大對象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對象”,寫程序的時候應(yīng)當(dāng)避免),經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來“安置”它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數(shù),令大于這個設(shè)置值的對象直接在老年代分配。這樣做的目的是避免在Eden區(qū)及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復(fù)制(復(fù)習(xí)一下:新生代采用復(fù)制算法收集內(nèi)存)。

執(zhí)行下面代碼中的testPretenureSizeThreshold()方法后,我們看到Eden空間幾乎沒有被使用,而老年代的10MB空間被使用了40%,也就是4MB的allocation對象直接就分配在老年代中,這是因為PretenureSizeThreshold被設(shè)置為3MB(就是3145728,這個參數(shù)不能像-Xmx之類的參數(shù)一樣直接寫3MB),因此超過3MB的對象都會直接在老年代進(jìn)行分配。

注意 PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認(rèn)識這個參數(shù),Parallel Scavenge收集器一般并不需要設(shè)置。如果遇到必須使用此參數(shù)的場合,可以考慮ParNew加CMS的收集器組合。

private static final int _1MB = 1024 * 1024;/**
 * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 * -XX:PretenureSizeThreshold=3145728
 */public static void testPretenureSizeThreshold() { ?byte[] allocation;
 ?allocation = new byte[4 * _1MB]; ?//直接分配在老年代中
}

?

運行結(jié)果:

Heap
 def new generation ? total 9216K, used 671K [0x029d0000, 0x033d0000, 0x033d0000)
 ?eden space 8192K, ? 8% used [0x029d0000, 0x02a77e98, 0x031d0000)
 ?from space 1024K, ? 0% used [0x031d0000, 0x031d0000, 0x032d0000)
 ?to ? space 1024K, ? 0% used [0x032d0000, 0x032d0000, 0x033d0000)
 tenured generation ? total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
 ? the space 10240K, ?40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
 compacting perm gen ?total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)
 ? the space 12288K, ?17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)
No shared spaces configured.

?

長期存活的對象將進(jìn)入老年代

既然虛擬機采用了分代收集的思想來管理內(nèi)存,那么內(nèi)存回收時就必須能識別哪些對象應(yīng)放在新生代,哪些對象應(yīng)放在老年代中。為了做到這點,虛擬機給每個對象定義了一個對象年齡(Age)計數(shù)器。如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設(shè)為1。對象在Survivor區(qū)中每“熬過”一次Minor GC,年齡就增加1歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold設(shè)置。

讀者可以試試分別以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15兩種設(shè)置來執(zhí)行下面代碼中的testTenuringThreshold()方法,此方法中的allocation1對象需要256KB內(nèi)存,Survivor空間可以容納。當(dāng)MaxTenuringThreshold=1時,allocation1對象在第二次GC發(fā)生時進(jìn)入老年代,新生代已使用的內(nèi)存GC后非常干凈地變成0KB。而MaxTenuringThreshold=15時,第二次GC發(fā)生后,allocation1對象則還留在新生代Survivor空間,這時新生代仍然有404KB被占用。

private static final int _1MB = 1024 * 1024;/**
 * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
 * -XX:+PrintTenuringDistribution
 */@SuppressWarnings("unused")public static void testTenuringThreshold() { ?byte[] allocation1, allocation2, allocation3;
 ?allocation1 = new byte[_1MB / 4]; 
 ? // 什么時候進(jìn)入老年代取決于XX:MaxTenuringThreshold設(shè)置
 ?allocation2 = new byte[4 * _1MB];
 ?allocation3 = new byte[4 * _1MB];
 ?allocation3 = null;
 ?allocation3 = new byte[4 * _1MB];
}

?

以MaxTenuringThreshold=1參數(shù)來運行的結(jié)果:

[GC [DefNew
Desired Survivor size 524288 bytes, new threshold 1 (max 1)
- age ? 1: ? ? 414664 bytes, ? ? 414664 total
: 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
[GC [DefNew
Desired Survivor size 524288 bytes, new threshold 1 (max 1)
: 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation ? total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
 ?eden space 8192K, ?51% used [0x029d0000, 0x02de4828, 0x031d0000)
 ?from space 1024K, ? 0% used [0x031d0000, 0x031d0000, 0x032d0000)
 ?to ? space 1024K, ? 0% used [0x032d0000, 0x032d0000, 0x033d0000)
 tenured generation ? total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)
 ? the space 10240K, ?43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)
 compacting perm gen ?total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
 ? the space 12288K, ?17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

?

以MaxTenuringThreshold=15參數(shù)來運行的結(jié)果:

[GC [DefNew
Desired Survivor size 524288 bytes, new threshold 15 (max 15)
- age ? 1: ? ? 414664 bytes, ? ? 414664 total
: 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew
Desired Survivor size 524288 bytes, new threshold 15 (max 15)
- age ? 2: ? ? 414520 bytes, ? ? 414520 total
: 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation ? total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)
 ?eden space 8192K, ?51% used [0x029d0000, 0x02de4828, 0x031d0000)
 ?from space 1024K, ?39% used [0x031d0000, 0x03235338, 0x032d0000)
 ?to ? space 1024K, ? 0% used [0x032d0000, 0x032d0000, 0x033d0000)
 tenured generation ? total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)
 ? the space 10240K, ?40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)
 compacting perm gen ?total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
 ? the space 12288K, ?17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

?

動態(tài)對象年齡判定

為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機并不是永遠(yuǎn)地要求對象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到MaxTenuringThreshold中要求的年齡。

執(zhí)行下面代碼中的testTenuringThreshold2()方法,并設(shè)置-XX:MaxTenuringThreshold=15,會發(fā)現(xiàn)運行結(jié)果中Survivor的空間占用仍然為0%,而老年代比預(yù)期增加了6%,也就是說,allocation1、allocation2對象都直接進(jìn)入了老年代,而沒有等到15歲的臨界年齡。因為這兩個對象加起來已經(jīng)到達(dá)了512KB,并且它們是同年的,滿足同年對象達(dá)到Survivor空間的一半規(guī)則。我們只要注釋掉其中一個對象new操作,就會發(fā)現(xiàn)另外一個就不會晉升到老年代中去了。

private static final int _1MB = 1024 * 1024;/**
 * VM參數(shù):-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
 * -XX:+PrintTenuringDistribution
 */@SuppressWarnings("unused")public static void testTenuringThreshold2() { ?byte[] allocation1, allocation2, allocation3, allocation4;
 ?allocation1 = new byte[_1MB / 4]; ? ?// allocation1+allocation2大于survivo空間一半
 ?allocation2 = new byte[_1MB / 4]; 
 ?allocation3 = new byte[4 * _1MB];
 ?allocation4 = new byte[4 * _1MB];
 ?allocation4 = null;
 ?allocation4 = new byte[4 * _1MB];
}

?

運行結(jié)果:

[GC [DefNew
Desired Survivor size 524288 bytes, new threshold 1 (max 15)
- age ? 1: ? ? 676824 bytes, ? ? 676824 total
: 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
[GC [DefNew
Desired Survivor size 524288 bytes, new threshold 15 (max 15)
: 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
 def new generation ? total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)
 ?eden space 8192K, ?51% used [0x029d0000, 0x02de4828, 0x031d0000)
 ?from space 1024K, ? 0% used [0x031d0000, 0x031d0000, 0x032d0000)
 ?to ? space 1024K, ? 0% used [0x032d0000, 0x032d0000, 0x033d0000)
 tenured generation ? total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)
 ? the space 10240K, ?46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)
 compacting perm gen ?total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)
 ? the space 12288K, ?17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)
No shared spaces configured.

?

空間分配擔(dān)保

在發(fā)生Minor GC之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險的;如果小于,或者HandlePromotionFailure設(shè)置不允許冒險,那這時也要改為進(jìn)行一次Full GC。

下面解釋一下“冒險”是冒了什么風(fēng)險,前面提到過,新生代使用復(fù)制收集算法,但為了內(nèi)存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當(dāng)出現(xiàn)大量對象在Minor GC后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對象都存活),就需要老年代進(jìn)行分配擔(dān)保,把Survivor無法容納的對象直接進(jìn)入老年代。與生活中的貸款擔(dān)保類似,老年代要進(jìn)行這樣的擔(dān)保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來在實際完成內(nèi)存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經(jīng)驗值,與老年代的剩余空間進(jìn)行比較,決定是否進(jìn)行Full GC來讓老年代騰出更多空間。

取平均值進(jìn)行比較其實仍然是一種動態(tài)概率的手段,也就是說,如果某次Minor GC存活后的對象突增,遠(yuǎn)遠(yuǎn)高于平均值的話,依然會導(dǎo)致?lián)J。℉andle Promotion Failure)。如果出現(xiàn)了HandlePromotionFailure失敗,那就只好在失敗后重新發(fā)起一次Full GC。雖然擔(dān)保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關(guān)打開,避免Full GC過于頻繁,參見下面代碼,請在JDK 6 Update 24之前的版本中運行測試。

private static final int _1MB = 1024 * 1024;/**
 * VM參數(shù):-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
 */@SuppressWarnings("unused")public static void testHandlePromotion() { ?byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
 ?allocation1 = new byte[2 * _1MB];
 ?allocation2 = new byte[2 * _1MB];
 ?allocation3 = new byte[2 * _1MB];
 ?allocation1 = null;
 ?allocation4 = new byte[2 * _1MB];
 ?allocation5 = new byte[2 * _1MB];
 ?allocation6 = new byte[2 * _1MB];
 ?allocation4 = null;
 ?allocation5 = null;
 ?allocation6 = null;
 ?allocation7 = new byte[2 * _1MB];
}

?

以HandlePromotionFailure = false參數(shù)來運行的結(jié)果:

[GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
[GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

以HandlePromotionFailure = true參數(shù)來運行的結(jié)果:
[GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

?

在JDK 6 Update 24之后,這個測試結(jié)果會有差異,HandlePromotionFailure參數(shù)不會再影響到虛擬機的空間分配擔(dān)保策略,觀察OpenJDK中的源碼變化(見下面代碼),雖然源碼中還定義了HandlePromotionFailure參數(shù),但是在代碼中已經(jīng)不會再使用它。JDK 6 Update 24之后的規(guī)則變?yōu)橹灰夏甏倪B續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會進(jìn)行Minor GC,否則將進(jìn)行Full GC。

bool TenuredGeneration::promotion_attempt_is_safe(size_t
max_promotion_in_bytes) const{ ? ?// 老年代最大可用的連續(xù)空間
 ? ?size_t available = max_contiguous_available(); ? ?// 每次晉升到老年代的平均大小
 ? ?size_t av_promo = (size_t) gc_stats()->avg_promoted()->padded_average(); ? ?// 老年代可用空間是否大于平均晉升大小,或者老年代可用空間是否大于當(dāng)此GC時新生代所有對象容量
 ? ?bool res = (available >= av_promo) || (available >=
 ? ? ? ? ? ?max_promotion_in_bytes); ? ?return res;
}

?


發(fā)表
26906人 簽到看排名