Java和C++之間有一堵由內(nèi)存動態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”,墻外面的人想進來,墻里面的人想出來。
對象已死嗎
在堆里放著的Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事就是要確定這些對象中哪些還“存活”著,哪些已經(jīng)“死去”(即不可能再被任何途徑使用的對象)。
引用計數(shù)法
客觀的講,引用計數(shù)法的實現(xiàn)簡單,判定效率也很高,在大多數(shù)情況下它都是一個不錯的算法,也有一些比較著名的案例,例如微軟的COM(Component Object Model)技術(shù)、使用ActionScript3的FlashPlayer、Python語言等都使用了引用計數(shù)算法來進行內(nèi)存管理。但是,至少在主流的JVM中沒有選用引用計數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間相互引用的問題。
可達性分析算法
在主流的商用程序語言(Java、C#等)的主流實現(xiàn)中,都是稱通過可達性分析來判斷對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些細節(jié)開始向下搜索,搜索所走過的路徑成為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連的時候,用圖論的話來說,就是從GC Roots到這個對象不可達的時候,則證明此對象是不可用的。
在Java語言中,可作為GC Roots的對象包括下面幾種:
- 虛擬機棧(棧幀中本地變量表)中引用的對象。
- 方法區(qū)中類靜態(tài)屬性引用的對象。
- 方法區(qū)中常量引用的對象。
- 本地方法棧中JNI引用的對象。
再談引用
無論是通過引用技術(shù)算法判斷對象的引用數(shù)量,還是通過可達性分析算法判斷對象的引用鏈是否可達,判斷對象是否存活都與“引用”有關(guān)。在JDK1.2之前,Java中的引用定義很傳統(tǒng):如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個引用。這種定義很純粹,但是太狹隘,一個對象在這種定義下只有被引用或者沒有被引用兩種狀態(tài),對于如何描述“食之無味,棄之可惜”的雞肋對象就顯得無能為力了。
在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用,這4種引用強度依次逐步減弱。
- 強引用就是指在程序代碼之中普遍存在的,類似“Object obj = new Object()”這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
- 軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出OOM。在JDK1.2之后,提供了SoftReference來實現(xiàn)軟引用。
- 弱引用也可以用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前。當垃圾收集器工作時,無論當前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。在JDK1.2之后,提供了WeakReference來實現(xiàn)弱引用。
- 虛引用也稱幽靈引用或者幻影引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存周期構(gòu)成影響,也無法通過虛引用來取的一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK1.2之后,提供了PhantomReference來實現(xiàn)虛引用。
生存還是死亡
即使在可達性分析算法中不可達的對象,也并非是非死不可的,這個時候它們暫時處于“緩刑”階段,要真正宣告一個對象死亡,至少要經(jīng)理兩次標記過程:如果對象在進行可達性分析后發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且運行一次篩選,篩選的條件是此對象是否有必要執(zhí)行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機吊用過,虛擬機將這兩種情況都視為“沒有必要執(zhí)行”。
如果這個對象被判定為有必要執(zhí)行finalize()方法,那么這個對象將會防止在一個叫F-Queue的隊列中,并在稍后由一個由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行它。這里所謂的執(zhí)行是指虛擬機會觸發(fā)這個方法,但并不承諾會等待它運行結(jié)束,這樣做的原因是,如果一個對象在finalize()方法中執(zhí)行緩慢,或者發(fā)生了死循環(huán),或者更極端的情況,將很有可能會導致F-Queue隊列中其他對象永久處于等待,甚至導致整個內(nèi)存回收系統(tǒng)崩潰。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后GV將對F-Queue中的對象進行第二次小規(guī)模標記,如果對象要在finalize()中成功拯救自己,只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,比如把自己(this)賦值給某個類變量或者對象的成員變量,那再第二次標記時它將被移除出“即將回收”的集合:如果對象這個時候還沒有逃脫,那基本上他就真的被回收了。
回收方法區(qū)
很多人認為方法區(qū)(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,JVM規(guī)范中確定說過可以不要求虛擬機在方法區(qū)實現(xiàn)垃圾收集,而且在方法區(qū)中進行垃圾收集的性價比一般比較低:在堆中,尤其是在新生代中,常規(guī)應(yīng)用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的來及收集效率遠低于此。
永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收為例,加入一個字符串“abc”已經(jīng)進入了常量池,但是當前系統(tǒng)沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發(fā)生內(nèi)存回收,而且必要的話,這個“abc”常量就會被系統(tǒng)清理出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與類似。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是無用的類的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是無用的類:
- 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類的任何實例。
- 加載該類的ClassLoader已經(jīng)被回收
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收,這里說的僅僅是可以,而并不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassbc參數(shù)進行控制。
在大量使用反射、動態(tài)代理、CGLib等ByteCode框架、動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。
垃圾收集算法
由于垃圾收集算法的實現(xiàn)涉及大量的程序細節(jié),而且各個平臺的虛擬機操作內(nèi)存的方法又各不相同,因此本節(jié)不打算過多討論算法的實現(xiàn),只是介紹幾種算法的思想及其發(fā)展過程。
標記-清除算法
最基礎(chǔ)的收集算法是“標記-清除”算法,如同他的名字一樣,算法分為“標記”和“清除”兩個階段: 首先標記處所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象。之所以說它是最基礎(chǔ)的收集算法,是因為后續(xù)的收集算法都是基于這種思路并對其不足進行改進而得到的。它的不足:一是效率問題,標記和清除兩個過程的效率并不高;另一個是控件問題,標記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大的對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。
復制算法
為了解決效率問題,一種稱為復制的收集算法出現(xiàn)了,它將可用內(nèi)存按照容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內(nèi)存用光了,就將還存活的對象復制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次性清理掉。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。只是這種算法的代價是將內(nèi)存縮小為原來的一半,未免太高了一點。
現(xiàn)在的商業(yè)虛擬機都采用了這種收集算法來回收新生代,IBM公司的專門研究表明,新生代的對象98%是朝生夕死的,并不需要按照1:1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中的一塊Survivor。當回收時,將Eden和Survivor中還存活著的對象一次性的復制到哦另一款Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90%(80%+10%),只有10%的內(nèi)存會被“浪費”。當然98%的對象可回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor的空間不夠用時,需要依賴其他內(nèi)存(這里指的是老年代)進行分配擔保。
標記-整理算法
復制收集算法在對象存活率較高時要進行較多的復制操作,效率就會降低。更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代不能直接用這種算法。
根據(jù)老年代的特點,有人提出了“標記-整理”算法,標記過程仍然與“標記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的UI小向一端移動,然后直接清理掉端便捷意外的內(nèi)存。
HotSpot的算法實現(xiàn)
在HotSpot虛擬機上實現(xiàn)這些算法時,必須對算法的執(zhí)行效率有嚴格的考量,才能保證虛擬機高效運行。
枚舉根節(jié)點
從可達性分析中從GCRoots節(jié)點找引用鏈這個操作為例,可作為GCRoots的節(jié)點主要在全局性的引用(例如常量或類靜態(tài)屬性)與執(zhí)行上下文(例如棧幀中的本地變量表)中,現(xiàn)在很多應(yīng)用僅僅方法區(qū)就有數(shù)百M,如果要逐個檢查這里面的引用,那么必然會消耗很多時間。
另外,可達性分析對執(zhí)行時間的敏感還體現(xiàn)在GC停頓上,因為這項分析工作必須在一個能確保一致性的快照中進行,這里的一致性是指在整個分析期間整個執(zhí)行系統(tǒng)看起來就像凍結(jié)在某個時間點上,不可以出現(xiàn)分析過程中對象引用關(guān)系還在不斷變化的情況,該點不滿足的話分析結(jié)果準確性就無法得到保障。這點是導致GC進行時必須挺多所有Java執(zhí)行線程的其中一個重要原因,Sun將這件事成為“Stop The World”,即使是在號稱幾乎不會發(fā)生停頓的CMS收集器中,枚舉根節(jié)點時也是必須要停頓的。
由于目前的主流Java虛擬機使用的都是準確式GC,所以當執(zhí)行系統(tǒng)停頓下來后,并不需要一個不漏的檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機應(yīng)當是有辦法直接得知哪些地方存放著對象引用。在HotSpot的實現(xiàn)中,是使用一組成為OopMap的數(shù)據(jù)結(jié)構(gòu)來達到這個目的的,在類加載完成的時候,HotSpot就把對象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些信息了。
安全點
在OopMap的協(xié)助下,HotSpot可以快速且準確的完成GCRoots枚舉,但一個很現(xiàn)實的問題隨之而來:可能導致引用關(guān)系變化,或者說OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對應(yīng)的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得很高。
實際上,HotSpot也的確沒有為每條指令都生成OopMap,前面已經(jīng)提到,只是在特定的位置記錄了這些信息,這些位置成為安全點,即程序執(zhí)行時并非在所有地方都能停頓下來開始GC,只有在到達安全點時才能暫停。Safepoint的選定既不能太少以致于讓GC等待的時間太長,也不能過于頻繁以致于過分增加運行時的負荷。所以安全點的選定基本上是以程序“是否具有讓程序長時間執(zhí)行的特征”為標準進行選定的,因為每條指令執(zhí)行的時間都非常短暫,程序不太可能因為指令流長度太長這個原因而過長時間運行,“長時間執(zhí)行”的最明顯特征就是指令序列復用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等,具有這些功能的指令才會產(chǎn)生安全點。
對于Safepoint,另一個需要考慮的問題就是如何在GC發(fā)生時讓所有線程(不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點上再停頓下來。這里有兩種方案可供算則:搶先式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)。
- 其中搶先式中斷不需要線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,首先把所有線程全部中斷,如果發(fā)現(xiàn)有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。現(xiàn)在幾乎沒有虛擬機實現(xiàn)才有搶先式中斷來暫停線程從而響應(yīng)GC。
- 而主動式中斷的思想是當GC需要中斷線索的時候,不直接對線程操作,僅僅簡單的設(shè)置一個標志,各個線程執(zhí)行主動去輪訓這個標志,發(fā)現(xiàn)中斷標志為真時就自己中斷掛起。輪詢標志的地方和安全點是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方。
安全區(qū)域
使用Safepoint似乎已經(jīng)完美的解決了如何進入GC的問題,但實際情況卻并不一定。Safepoint機制保證了程序執(zhí)行時,在不太長的時間內(nèi)就會遇到可進入GC的Safepoint。但是,程序不執(zhí)行的時候呢?所謂程序不執(zhí)行就是沒有分配CPU時間,典型的例子就是線程處于Sleep狀態(tài)或者Blocked狀態(tài),這個時候線程無法響應(yīng)JVM的中斷請求,走到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配CPU時間。對于這種情況,就需要安全區(qū)域來解決。
安全區(qū)域是指一段代碼片段之中,引用關(guān)系不會發(fā)生變化。在這個區(qū)域中的任意地方開始GC都是安全的。我們也可以把SafeRegion看做是被擴展的Safepoint。
在線程執(zhí)行到Safe Region中的代碼時,首先標識自己已經(jīng)進入了SafeRegion,那樣,當在這段時間里JVM要發(fā)起GC時,就不用管標識自己為SafeRegion狀態(tài)的線程了。在線程要離開SafeRegion時,它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點枚舉,或者整個GC過程,如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開SafeRegion的信號為止。
垃圾收集器
因為內(nèi)存回收如何進行是由虛擬機所采用的GC收集器決定的,而通常虛擬機中往往不止有一種GC收集器。
如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。Java虛擬機規(guī)范中對垃圾收集器應(yīng)該如何實現(xiàn)并沒有任何規(guī)定,因此不同的廠商、不同版本的虛擬機所提供的垃圾收集器都可能會有很大差別,并且一般都會提供參數(shù)供用戶根據(jù)自己的應(yīng)用特點和要求組合出各個年代所使用的收集器。這里討論的收集器基于JDK1.7 Update14之后的HotSpot虛擬機,在這個版本中正式提供了商用的G1收集器,這個虛擬機包含的所有收集器如下
圖中展示了7種不同分代的收集器,如果兩個收集器之間存在連線,就說明他們可以搭配使用。虛擬機所處的區(qū)域,則表示它是屬于新生代還是老年代的收集器。我們雖然要對各個收集器進行比較,但是并非為了選擇一種最好的收集器。因為知道現(xiàn)在為止還沒有最好的收集器出現(xiàn),更加沒有萬能的收集器,所以我們選擇的知識因軌道具體應(yīng)用場景下最合適的收集器。
Serial收集器
Serial收集器是最基本、發(fā)展歷史最悠久的收集器,在JDK1.3.1之前是虛擬機新生代收集的唯一選擇。看名字就知道,這個收集器是一個單線程的收集器,但它的單線程的意義并不僅僅說明它使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,知道它收集結(jié)束。“Stop the World”這個名字也許聽起來很酷,但這項工作實際上是由虛擬機在后臺自動發(fā)起和自動完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對很多應(yīng)用來說都是難以接受的。圖中示意了Serial/Serial Old收集器的運行過程。
從JDK1.3開始,一直到現(xiàn)在最新的JDK1.7,HotSpot虛擬機開發(fā)團隊為清除或者減少工作線程銀內(nèi)存回收而導致停頓的努力一直在進行著,從Serial收集器到Parallel收集器,再到CMS乃至GC收集器的最前沿成果G1收集器,我們看到了一個個越來越優(yōu)秀的收集器出現(xiàn),用戶線程的收集時間不斷縮短,但是仍然沒有完全消除。
它依然是虛擬機運行在Client模式下默認新生代的收集器。它有著優(yōu)于其他收集器的地方:
- 與其他收集器的單線程比,它簡單高效
- 對于限定單個CPU的環(huán)境來說,優(yōu)于沒有線程交互的開銷,專心做垃圾收集,因此獲得最高的單線程收集效率
在用戶的桌面應(yīng)用場景,分配給虛擬機管理的內(nèi)存一般來說不會很大,收集幾十兆甚至幾百兆的新生代,停頓時間完全可以控制在幾十毫秒以內(nèi),只要不頻繁發(fā)生,這點停頓還是可以接受的。所以Serial收集器對于運行在Client模式下的虛擬機來說是一個很好的選擇。
ParNew收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集之外,其余行為包括Serial收集器可用的所有控制參數(shù)、收集算法、Stop the World、對象分配規(guī)則、回收策略等都與Serial收集器完全一樣,在實現(xiàn)上,這兩個收集器也公用了相當多的代碼。
ParNew收集器除了多線程收集之外,其他與Serial收集器相比并沒有太多創(chuàng)新之處,但它卻是很多運行在Server模式下虛擬機中首選的新生代收集器,其中有一個與性能無關(guān)但很重要的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作。在JDK1.5時期,HotSpot推出了一款在強交互中幾乎可任務(wù)有劃時代意義的垃圾收集器,CMS收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的并發(fā)收集器,它第一次實現(xiàn)了讓垃圾收集線程與用戶線程同時工作。
不幸的是,CMS作為老年代的收集器,無法與JDK1.4.0中存在的新生代收集器Parallel Scavenge配合工作,所以在JDK1.5中使用CMS來手機老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。ParNew收集器也是使用-XX:UseConcMarkSweepGC選項后的默認新生代收集器,也可以使用-XX:UsePerNewGC來強制指定它。
PerNew收集器在單CPU的環(huán)境中絕對不會比Serial收集器的效果更好,甚至優(yōu)于存在線程交互的開銷,該收集器在通過超線程技術(shù)實現(xiàn)的兩個CPU的環(huán)境中都不能百分之百的保證可以超越Serial收集器。當然,隨著可以使用的CPU的數(shù)量越來越多,它對于GC時系統(tǒng)資源的有效利用還是會很有好處的。它默認開啟的線程數(shù)量與CPU的數(shù)量相同,在CPU非常多的環(huán)境下,可以使用-XX:ParallelGCThreads參數(shù)來限制垃圾收集的線程數(shù)。
并發(fā)和并行
- 并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍然處于等待狀態(tài)。
- 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時執(zhí)行,但不一定是并行的,可能會交替執(zhí)行,用戶程序在繼續(xù)運行,而垃圾收集程序云星宇另一個CPU上。
Parallel Scavenge收集器
Parallel Scavenge收集器是一個新生代收集器,它也是使用復制算法的收集器,又是并行的多線程收集器,看上去和PerNew都一樣,那它有什么特別之處?
Parallel Scavenge收集器的特點是它的關(guān)注點和其他收集器不同,CMS等收集器的關(guān)注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。所謂吞吐量就是CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間÷(運行用戶代碼時間+垃圾收集時間),虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,吞吐量就是99%。
停頓的時間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗,而高吞吐量則可以高效的利用CPU時間,盡快完成程序的運算任務(wù),主要適合在后臺運算而不需要太多交互的任務(wù)。
Parallel Scavenge收集器提供了兩個參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis闡述一斤直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)。
- MaxGCPauseMillis參數(shù)允許的值是一個大于0的毫秒數(shù),收集器將盡可能地保證內(nèi)存回收話費的時間不超過設(shè)定值。不過不要認為如果把這個參數(shù)的值設(shè)置的小一點就能使得系統(tǒng)的垃圾收集速度變快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統(tǒng)把新生代調(diào)小一些,收集300MB新生代肯定比收集500MB快,這也直接導致垃圾收集發(fā)生得更頻繁一些,原來10s收集一次、每次停頓100ms,現(xiàn)在變成5s手機一次、每次停頓70ms。停頓時間確實是在下降,但吞吐量也在下降。
- GCTimeRatio參數(shù)的值應(yīng)當是一個大于0且小于100的整數(shù),也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數(shù)。如果把此參數(shù)設(shè)為19,那允許的最大GC時間就占總時間的5%,即1÷(1+19),默認值是99%,就是允許最大1%,即1÷(1+99)的垃圾收集時間。
由于與吞吐量關(guān)系密切,Parallel Scavenge收集器也經(jīng)常被稱為吞吐量優(yōu)先收集器。除了上述兩個參數(shù)外,Parallel Scavenge收集器還有一個參數(shù)-XX:UseAdaptiveSizePolicy值得關(guān)注。這是一個開關(guān)參數(shù),當這個參數(shù)打開之后,就不需要手工指定新生代的大小、Eden與Survivor去的比例、晉升老年代對象大小等細節(jié)參數(shù),虛擬機會根據(jù)當前系統(tǒng)的運行狀況收集性能監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時間或者最大的吞吐量,這種調(diào)節(jié)方式成為GC自適應(yīng)的調(diào)節(jié)策略。
Serial Old收集器
Serial Old收集器是Serial收集器的老年版本,它同樣是一個單線程收集器,使用“標記-整理”算法。這個收集器的主要意義也是在于給Clinet模式下的虛擬機使用。如果在Server模式下,它主要還有兩大用途:
- 一種是在JDK1.5的版本中與Parallel Scavenge收集器搭配使用,另一種用途就是作為CMS收集器的后背預(yù)案,在并發(fā)手機發(fā)生Concurrent Mode Failure時使用。工作流程如下。
Parallel Old收集器
Parallel Old是 Parallel Scavenge收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器是在JDK1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是,如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old收集器外別無選擇。由于老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的拖累,使用了Parallel Scavenge收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果,由于單線程的老年代收集中無法充分利用服務(wù)器多CPU的處理能力,在老年代很大而且硬件比較高級的環(huán)境中,這種組合的吞吐量甚至不一定有ParNew+CMS的組合好。
直到Parallel Old收集器的出現(xiàn)后,“吞吐量優(yōu)先”收集器終于有了比較名副其實的應(yīng)用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge+Parallel Old收集器。
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓的時間最短,給用戶帶來較好的體驗。CMS收集器就非常符合這類應(yīng)用的需求。
CMS收集器是基于“標記-清除”算法實現(xiàn)的,它的運作過程相對于前面幾種收集器來說更復雜一些,整個過程分為4個步驟:
- 初始標記(CMS initial mark)
- 并發(fā)標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
其中,初始標記、重新標記著兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快,并發(fā)標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比并發(fā)標記的時間短。
由于整個過程中耗時最長的并發(fā)標記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,MCS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。
CMS是一款優(yōu)秀的收集器,它的主要優(yōu)點在名字上應(yīng)體現(xiàn)出來了:并發(fā)手機、低停頓,Sun公司的一些官方文檔中也稱之為并發(fā)低停頓收集器(Concurrent Low Pause Collector)。但是CMS還遠沒達到完美的程度,以為它有如下3個缺點:
- CMS收集器對CPU資源非常敏感。其實,面向并發(fā)設(shè)計的程序都對CPU資源比較敏感。在并發(fā)階段,它雖然不會導致用戶線程停頓,但是會因為占用了一部分線程或者CPU資源而導致應(yīng)用程序變慢,總吞吐量會降低。CMS默認啟動的回收線程數(shù)量是(CPU數(shù)量+3)÷4,也就是當CPU在4個以上時,并發(fā)回收時垃圾收集線程不少于25%的CPU資源,并隨著CPU數(shù)量的增加而下降。但是當CPU不足4個的時候,CMS對用戶程序的影響可能會變大,如果本來CPU負載就比較大,還要分出來一半的運算能力去執(zhí)行收集器線程,就可能導致用戶程序的執(zhí)行速度忽然降低50%,其實也是令人無法接受的。
- CMS收集器無法處理浮動垃圾,可能出現(xiàn)“Concurrent Mode Failure”失敗而導致另一次FullGC的產(chǎn)生。由于CMS并發(fā)清理階段用戶線程還在運行著,伴隨程序運行自然就還會有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為浮動垃圾。也是由于在垃圾收集階段用戶線程還需要運行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎全被填滿了再進行收集,需要預(yù)留一部分空間提供并發(fā)收集時的程序運作使用。在JDK1.5的默認設(shè)置下,CMS收集器與老年代使用了68%的控件后就會被激活,這是一個偏保守的設(shè)置,如果在應(yīng)用中老年代增長不是太快,可以適當調(diào)高參數(shù)-XX:CMSInitiatingQccupancyFraction的值來提高觸發(fā)百分比,以便降低內(nèi)存回收次數(shù)從而獲取更好的性能,在JDK1.6中,CMS收集器的啟動閾值已經(jīng)提升到了92%。要是CMS運行期間預(yù)留的內(nèi)存無法滿足程序需要,就會出現(xiàn)一次“Concurrent Mode Failure”失敗,這時,虛擬機將啟動后背預(yù)案:臨時啟用Serial Old 收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所以說參數(shù)-XX:CMSInitiatingOccupancyFraction設(shè)置的太高很容易導致大量“Concurrent Mode Failure”失敗,性能反而會降低。
- 最后一個缺點,CMS是一款基于“標記-清除”算法實現(xiàn)的收集器,收集結(jié)束時會產(chǎn)生大量的空間碎片,這將會給大對象分配帶來很大的麻煩,往往會出現(xiàn)老年代還有扥大剩余空間,但是無法找到足夠大的連續(xù)空間來分配給當前對象,不得不提前觸發(fā)一次FullGC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關(guān)參數(shù)(默認是開啟的),用于CMS收集器頂不住要進行FullGC時開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理的過程是無法并發(fā)的,空間碎片問題沒有了,但停頓時間不得不變長了。虛擬機設(shè)計者還提供了另一個參數(shù)-XX:CMSFullGCsBeforeCompaction,這個參數(shù)是用于設(shè)置執(zhí)行多少次不壓縮FullGC后,跟著來一次帶壓縮的,默認值是0,表示每次進入FullGC時都進行碎片整理。
G1收集器
G1(Garbage-First)收集器是當今收集器技術(shù)發(fā)展的最前沿成果之一,早在JDK1.7剛剛確立項目目標,Sun公司給JDK1.7RoadMap里面,它就被視為JDK1.7中HotSpot虛擬機的一個重要進化特征。從JDK6u14中開始就有Early Access版本的G1收集器供開發(fā)人員實驗、試用,由此G1收集器的“Experimental”狀態(tài)持續(xù)了數(shù)年時間,直至JDK7u4,Sun公司才認為它達到了足夠成熟的商用程度,移除了“Experimental”標識。
G1收集器是一款面向服務(wù)端應(yīng)用的垃圾收集器。HotSpot開發(fā)團隊富裕它的使命是未來可以替換掉JDK1.5中發(fā)布的CMS收集器。與其他GC收集器相比,G1具備以下特點:
- 并行與并發(fā):G1能充分利用多CPU、多核環(huán)境下的硬件優(yōu)勢,使用多個CPU或者多核CPU來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java核心線程的GC動作,G1收集器仍然可以通過并發(fā)的方式讓Java程序繼續(xù)運行。
- 分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新創(chuàng)建的對象和已經(jīng)存活了一段時間、熬過了多次GC的舊對象以獲取更好的收集效果。
- 空間整合:與CMS的“標記-清楚”算法不同,G1從整體上來看是基于“標記-整理”算法實現(xiàn)的收集器,從局部上來看是基于“復制”算法實現(xiàn)的。但無論如何,這兩種算法都意味著G1運作期間不會產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續(xù)內(nèi)存而提前觸發(fā)下一次GC。
- 可預(yù)測的停頓:這里G1相對于CMS的另一大優(yōu)勢,降低停頓時間的G1和CMS共同關(guān)注點,但G1除了追求低停頓外,還能簡歷可預(yù)測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒,這幾乎是實時Java(RTSJ)的垃圾收集器的特征了。
在G1之前的其他收集器進行收集的范圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器,Java堆的內(nèi)存布局就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區(qū)域,雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region的集合。
G1收集器之所以能建立可預(yù)測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區(qū)域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的價值大小,在后臺維護一個優(yōu)先列表,每次根據(jù)允許的收集時間,優(yōu)先回收價值最大的Region。這種使用Region劃分內(nèi)存空間以及有優(yōu)先級的區(qū)域回收方式,保證了G1收集器在有限時間內(nèi)可以獲取盡可能高的收集效率。
如果不計算維護Remembered Set的操作,G1收集器的運作大致可劃分為以下步驟:
- 初始標記(Initial Marking)
- 并發(fā)標記(Concurrent Marking)
- 最終標記(Final Marking)
- 篩選標記(Live Data Counting and Evacuation)
G1的前幾個步驟的運作過程和CMS有很多相似之處。初始標記階段僅僅只是標記一下GCRoots能直接關(guān)聯(lián)到的對象,并且修改TAMS(Next Top At Mark Start)的值,讓下一階段用戶程序并發(fā)運行時,能在正確可用的Region中創(chuàng)建新對象,這階段需要停頓線程,但耗時很短。并發(fā)標記階段是從GC Roots開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序并發(fā)執(zhí)行。而最終標記階段則是為了修正在并發(fā)標記期間因用戶程序繼續(xù)運作而導致標記產(chǎn)生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程Remembered Set中,這階段需要停頓線程,但是可并行執(zhí)行。最后在篩選回首階段首先對各個Region的回收價值和成本進行評估排序,根據(jù)用戶所期望的GC停頓時間來制定回收計劃。
垃圾收集器常用參數(shù)總結(jié)
- UseSerialGC:虛擬機運行在Client模式下的默認值,打開此開關(guān)后,使用Serial+Serial Old的收集器組合進行內(nèi)存回收
- UseParNewGC:打開此開關(guān)后,使用ParNew+Serial Old的收集器組合進行內(nèi)存回收
- UseConcMarkSweepGC:打開此開關(guān)后,使用ParNew+CMS+Serial Old的收集器組合內(nèi)存回收。Serial Old收集器將作為CMS收集器出現(xiàn)Concurrent Mode Failure失敗后的后備收集器使用
- UseParallelGC:虛擬機在Server模式下的默認值,打開此開關(guān)后,使用Parallel Scavenge+Serial Old的收集器組合進行內(nèi)存回收
- UseParallelOldGC:打開此開關(guān)后,使用Parallel Scavenge+Parallel Old的收集器組合進行內(nèi)回收
- SurvivorRatio:新生代中Eden區(qū)域與Survivor區(qū)域的容量比值,默認為8,代表Eden:Survivor=8:1
- PretenureSizeTheshold:直接晉升到老年代的對象大小,設(shè)置這個參數(shù)后,大于這個參數(shù)的對象將直接分配到老年代
- MaxTenuringTheshold:晉升到老年代的對象年齡。每個對象在堅持過一次Minor GC之后,年齡都增加1,當超過這個參數(shù)值時就進入老年代
- UseAdaptiveSizePolicy:動態(tài)調(diào)整Java堆中各個區(qū)域的大小以及進入老年代的年齡
- HandlePromotionFailure:是否允許分配擔保失敗,即老年代的剩余空間不足以應(yīng)付新生代的整個Eden和Survivor區(qū)的左右對象都存活的極端情況
- ParallelGCThreads:設(shè)置并行GC時進行內(nèi)存回收的線程數(shù)
- GCTimeRatio:GC時間占總時間的比率,默認值為99,即允許1%的GC時間。僅僅在使用Parallel Scavenge收集器時生效
- MaxGCPauseMillis:設(shè)置GC的最大停頓時間,僅在是使用Parallel Scavenge收集器時生效
- CMSInitiatingOccupancyFraction:設(shè)置CMS收集器在老年代空間被使用多少后觸發(fā)垃圾收集。默認值是68%。僅在使用CMS收集器時生效
- UseCMSCompactAtFullCollection:設(shè)置CMS收集器在完成垃圾收集后是否要進行一次內(nèi)存碎片整理。僅在使用CMS收集器時生效
- CMSFullGCsBeforeCompaction:設(shè)置CMS收集器在進行若干次垃圾收集后再次啟動一次內(nèi)存碎片整理。僅在使用CMS收集器時生效
JVM內(nèi)存分配與回收策略
Java技術(shù)體系中所提倡的自動內(nèi)存管理最終都可以歸結(jié)為自動化地解決了兩個問題:
給對象分配內(nèi)存以及回收分配給對象的內(nèi)存。
對象的分配,宏觀上講就是在堆上分配,但也可能經(jīng)過JIT編譯后被拆散為標量類型并間接地棧上分配,對象主要分配在新生代Eden區(qū)上,如果啟動了本地線程分配緩沖,將線程優(yōu)先在TLAB上分配。少數(shù)情況下可能會直接分配到老年代中,分配的規(guī)則并不是百分之百固定的,其細節(jié)取決于當前使用的是哪一種垃圾收集器組合,還有虛擬機中與內(nèi)存相關(guān)的參數(shù)設(shè)置。
對象優(yōu)先在Eden分配
大多數(shù)情況下,對象在新生代Eden區(qū)中分配。當Eden區(qū)沒有足夠空間進行分配時,虛擬機將發(fā)起一次MinorGC。
虛擬機提供了-XX:PrintGCDetails這個收集器日志參數(shù),告訴虛擬機在發(fā)生垃圾收集行為時打印內(nèi)存回收日志,并且在線程退出的時候輸出當前的內(nèi)存各區(qū)域分配情況,在實際應(yīng)用中,內(nèi)部才能回收日志一般是打印到文件后通過日志工具進行分析。
MinorGC和FullGC有什么不一樣:
- 新生代GC(MinorGC):指的是發(fā)生在新生代的垃圾收集操作,因為Java對象大多數(shù)都具備朝生夕滅的特性,所以MinorGC非常頻繁,一般回收速度也比較快
- 老年代GC(MajorGC/FullGC):指發(fā)生在老年代的GC,出現(xiàn)了MajorGC,進場會伴隨至少一次的MinorGC(但是在Parallel Scavenge收集器的收集策略里就有直接進行MajorGC的策略選擇過程)。MajorGC的速度一般會比MinorGC慢10倍以上。
大對象直接進入老年代
所謂大對象是指,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的就是那種很長的字符串或數(shù)組。大對象對于虛擬機的內(nèi)存分配來說是個壞消息,經(jīng)常出現(xiàn)大對象容易造成內(nèi)存還有不少空間時提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來安置他們。虛擬機提供了一個-XX:PretenureSizeThreshold參數(shù),令大于這個設(shè)置值的對象直接在老年代分配。這樣做的目的是避免在Eden區(qū)以及兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存復制
PretenureSizeThreshold參數(shù)只對Serial和ParNew兩款收集器有效,Parallel Scavenge收集器不認識這個參數(shù),Parallel Scavenge收集器一般并不需要設(shè)置。如果遇到必須使用此參數(shù)的場合,可以考慮ParNew+CMS的收集器組合。
長期存活的對象將進入老年代
既然虛擬機采用了分代收集的思路來管理內(nèi)存,那么內(nèi)存回收時就必須能識別哪些對象應(yīng)放在新生代,哪些對象應(yīng)放在老年代中。為了做到這點,虛擬機給每個對象定義了一個對象年齡計數(shù)器。如果對象在Eden出生并經(jīng)過第一次MinorGC后仍然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,并且對象年齡設(shè)為1。對象在Survivor區(qū)中每熬過一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,可以通過參數(shù)-XX:MaxTenuringThreshold設(shè)置。
動態(tài)對象年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機并不是永遠地要求對象的年齡必須達到MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
空間分配擔保
在發(fā)生MinorGC之前,虛擬機會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么MinorGC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設(shè)置值是否允許擔保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次MinorGC,盡管這次MinorGC是有風險的;如果小于,或者HandlePromotionFailure設(shè)置不允許毛線,那么這時也要改為進行一次FullGC。
新生代使用復制收集算法,但是為了內(nèi)存利用率,只使用其中一個Survivor空間來作為輪換備份,因此在出現(xiàn)大量對象在MinorGC后仍然存活的情況,最極端的情況是內(nèi)存回收后新生代的所有對象都存活,就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會存活下來在實際完成內(nèi)部才能回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經(jīng)驗值,與老年代的剩余空間進行比較,決定是否進行FullGC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態(tài)概率的手段,也就是說,如果其次MinorGC存活后的對象突增,遠遠高于平均值的話,依然會導致?lián)J。℉andle Promotion Failure)。如果出現(xiàn)了擔保失敗,那就只好在失敗后重新發(fā)起一次FullGC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都會將擔保失敗開關(guān)打開,避免FullGC過于頻繁。
小結(jié)
內(nèi)存回收與垃圾收集器在很多時候都是影響系統(tǒng)性能、并發(fā)能力的主要因素之一,虛擬機之所以提供多種不同的收集器以及提供大量的參數(shù)調(diào)節(jié),是因為只有根據(jù)實際應(yīng)用需求、實現(xiàn)方式選擇最優(yōu)的收集方式才能獲取更高的性能。沒有固定收集器、參數(shù)組合,也沒有最優(yōu)的調(diào)優(yōu)方法,虛擬機也就沒有什么必然的內(nèi)存回收行為。因此,學習虛擬機內(nèi)存知識,如果要到實踐調(diào)優(yōu)階段,那么必須了解每個具體收集器的行為、優(yōu)勢、劣勢、調(diào)節(jié)參數(shù)。