偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

一文讀懂Java垃圾回收機(jī)制原理

開發(fā) 后端
Java 內(nèi)存運(yùn)行時(shí)區(qū)域中的程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧隨線程而生滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。

[[383727]]

本文轉(zhuǎn)載自微信公眾號(hào)「碼農(nóng)私房話」,作者曹至梧。轉(zhuǎn)載本文請(qǐng)聯(lián)系碼農(nóng)私房話公眾號(hào)。

本文內(nèi)容源自閱讀《深入理解 Java 虛擬機(jī)》書籍后整理的筆記,內(nèi)容較多,概括了 Java 垃圾回收機(jī)制、垃圾回收器以及內(nèi)存分配策略等內(nèi)容,在了解 Java 垃圾回收機(jī)制前,建議先閱讀 Java 內(nèi)存區(qū)域。

Java 垃圾回收機(jī)制

1. 垃圾回收主要關(guān)注 Java 堆

圖摘自《碼出高效》

Java 內(nèi)存運(yùn)行時(shí)區(qū)域中的程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧隨線程而生滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來(lái)時(shí)就已知的(盡管在運(yùn)行期會(huì)由 JIT 編譯器進(jìn)行一些優(yōu)化),因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,不需要過多考慮回收的問題,因?yàn)榉椒ńY(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。

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

2. 判斷哪些對(duì)象需要被回收

有以下兩種方法:

  • 引用計(jì)數(shù)法給對(duì)象添加一引用計(jì)數(shù)器,被引用一次計(jì)數(shù)器值就加 1;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減 1;計(jì)數(shù)器為 0 時(shí),對(duì)象就是不可能再被使用的,簡(jiǎn)單高效,缺點(diǎn)是無(wú)法解決對(duì)象之間相互循環(huán)引用的問題。
  • 可達(dá)性分析算法通過一系列的稱為 "GC Roots" 的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的。此算法解決了上述循環(huán)引用的問題。

在Java語(yǔ)言中,可作為 GC Roots 的對(duì)象包括下面幾種:a. 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。b. 方法區(qū)中類靜態(tài)屬性引用的對(duì)象。c. 方法區(qū)中常量引用的對(duì)象。d. 本地方法棧中 JNI(Native方法)引用的對(duì)象。

作為 GC Roots 的節(jié)點(diǎn)主要在全局性的引用與執(zhí)行上下文中。要明確的是,tracing gc必須以當(dāng)前存活的對(duì)象集為 Roots,因此必須選取確定存活的引用類型對(duì)象。

GC 管理的區(qū)域是 Java 堆及方法區(qū),虛擬機(jī)棧、本地方法棧不被 GC 所管理,因此選用這些區(qū)域內(nèi)引用的對(duì)象作為 GC Roots,是不會(huì)被 GC 所回收的。

其中虛擬機(jī)棧和本地方法棧都是線程私有的內(nèi)存區(qū)域,只要線程沒有終止,就能確保它們中引用對(duì)象的存活,在方法區(qū)中類靜態(tài)屬性引用的對(duì)象顯然是存活的,常量引用的對(duì)象在當(dāng)前可能存活,也可能是 GC Roots 的一部分。

3. 強(qiáng)、軟、弱、虛引用

JDK1.2 以前,一個(gè)對(duì)象只有被引用和沒有被引用兩種狀態(tài)。

后來(lái),Java 對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)以及虛引用(Phantom Reference)4 種,這 4 種引用強(qiáng)度依次逐漸減弱。

  • 強(qiáng)引用就是指在程序代碼之中普遍存在的,類似"Object obj=new Object()"這類的引用,垃圾收集器永遠(yuǎn)不會(huì)回收存活的強(qiáng)引用對(duì)象。
  • 軟引用:還有用但并非必需的對(duì)象。在系統(tǒng) 將要發(fā)生內(nèi)存溢出異常之前 ,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。
  • 弱引用也是用來(lái)描述非必需對(duì)象的,被弱引用關(guān)聯(lián)的對(duì)象 只能生存到下一次垃圾收集發(fā)生之前 。當(dāng)垃圾收集器工作時(shí),無(wú)論內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。
  • 虛引用是最弱的一種引用關(guān)系。無(wú)法通過虛引用來(lái)取得一個(gè)對(duì)象實(shí)例 。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。

圖摘自《碼出高效》

4. 可達(dá)性分析算法

不可達(dá)的對(duì)象將暫時(shí)處于“緩刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過程:

  1. 如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒有與 GC Roots 相連接的引用鏈,那它將會(huì)被第一次標(biāo)記并且進(jìn)行一次篩選,篩選的條件是此對(duì)象是否有必要執(zhí)行 finalize() 方法。
  2. 當(dāng)對(duì)象沒有覆蓋 finalize() 方法,或者 finalize() 方法已經(jīng)被虛擬機(jī)調(diào)用過,虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”,直接進(jìn)行第二次標(biāo)記。
  3. 如果這個(gè)對(duì)象被判定為有必要執(zhí)行 finalize() 方法,那么這個(gè)對(duì)象將會(huì)放置在一個(gè)叫做 F-Queue 的隊(duì)列之中,并在稍后由一個(gè)由虛擬機(jī)自動(dòng)建立的、低優(yōu)先級(jí)的 Finalizer 線程去執(zhí)行它。

這里所謂的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法,但并不承諾會(huì)等待它運(yùn)行結(jié)束,因?yàn)槿绻粋€(gè)對(duì)象在 finalize() 方法中執(zhí)行緩慢,將很可能會(huì)一直阻塞 F-Queue 隊(duì)列,甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰,測(cè)試程序:

  1. public class FinalizerTest { 
  2.     public static FinalizerTest object; 
  3.     public void isAlive() { 
  4.         System.out.println("I'm alive"); 
  5.     } 
  6.   
  7.     @Override 
  8.     protected void finalize() throws Throwable { 
  9.         super.finalize(); 
  10.         System.out.println("method finalize is running"); 
  11.         object = this; 
  12.     } 
  13.   
  14.     public static void main(String[] args) throws Exception { 
  15.         object = new FinalizerTest(); 
  16.         // 第一次執(zhí)行,finalize方法會(huì)自救 
  17.         object = null
  18.         System.gc(); 
  19.   
  20.         Thread.sleep(500); 
  21.         if (object != null) { 
  22.             object.isAlive(); 
  23.         } else { 
  24.             System.out.println("I'm dead"); 
  25.         } 
  26.   
  27.         // 第二次執(zhí)行,finalize方法已經(jīng)執(zhí)行過 
  28.         object = null
  29.         System.gc(); 
  30.   
  31.         Thread.sleep(500); 
  32.         if (object != null) { 
  33.             object.isAlive(); 
  34.         } else { 
  35.             System.out.println("I'm dead"); 
  36.         } 
  37.     } 

引用自 Java GC的那些事

輸出如下:

  1. Copymethod finalize is running 
  2. I'm alive 
  3. I'm dead 

如果不重寫finalize(),輸出將會(huì)是:

  1. CopyI'm dead 
  2. I'm dead 

從執(zhí)行結(jié)果可以看出:

第一次發(fā)生 GC 時(shí),finalize() 方法的確執(zhí)行了,并且在被回收之前成功逃脫;第二次發(fā)生 GC 時(shí),由于 finalize() 方法只會(huì)被 JVM 調(diào)用一次,object 被回收。

值得注意的是,使用 finalize() 方法來(lái)“拯救”對(duì)象是不值得提倡的,它的運(yùn)行代價(jià)高昂,不確定性大,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序。finalize() 能做的工作,使用 try-finally 或者其它方法都更適合、及時(shí)。

5. Java 堆永久代的回收

永久代的垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無(wú)用的類。

  • 回收廢棄常量與回收 Java 堆中的對(duì)象非常類似。以常量池中字面量的回收為例,假如一個(gè)字符串"abc"已經(jīng)進(jìn)入了常量池中,但是當(dāng)前系統(tǒng)沒有任何一個(gè) String 對(duì)象是叫做"abc"的,也沒有其他地方引用了這個(gè)字面量,如果這時(shí)發(fā)生內(nèi)存回收,而且必要的話,這個(gè)"abc"常量就會(huì)被系統(tǒng)清理出常量池。常量池中的其他類(接口)、方法、字段的符號(hào)引用也與此類似。
  • 類需要同時(shí)滿足下面 3 個(gè)條件才能算是“無(wú)用的類”:

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

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

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

虛擬機(jī)可以對(duì)滿足上述 3 個(gè)條件的無(wú)用類進(jìn)行回收,這里說的僅僅是“可以”,而并不是和對(duì)象一樣,不使用了就必然會(huì)回收。

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

垃圾收集算法

一共有 4 種:

  • 標(biāo)記-清除算法
  • 復(fù)制算法
  • 標(biāo)記整理算法
  • 分代收集算法

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

最基礎(chǔ)的收集算法是“標(biāo)記-清除”(Mark-Sweep)算法,分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。

它的主要不足有兩個(gè):

  1. 效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高。
  2. 空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。

標(biāo)記—清除算法的執(zhí)行過程如下圖。

2. 復(fù)制算法

為了解決回收效率問題,一種稱為“復(fù)制”(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。

這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為了原來(lái)的一半。復(fù)制算法執(zhí)行過程如下圖:

現(xiàn)在的商業(yè)虛擬機(jī)都采用這種算法來(lái)回收新生代,IBM 研究指出新生代中的對(duì)象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例來(lái)劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。

當(dāng)回收時(shí),將 Eden 和 Survivor 中還存活著的對(duì)象一次性地復(fù)制到另外一塊的 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間。HotSpot 虛擬機(jī)默認(rèn) Eden:Survivor = 8:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的 90%(其中一塊 Survivor 不可用),只有 10% 的內(nèi)存會(huì)被“浪費(fèi)”。

當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下統(tǒng)計(jì)的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于 10% 的對(duì)象存活,當(dāng) Survivor 空間不夠用時(shí),需要依賴其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle Promotion)。

內(nèi)存的分配擔(dān)保就好比我們?nèi)ャy行借款,如果我們信譽(yù)很好,在 98% 的情況下都能按時(shí)償還,于是銀行可能會(huì)默認(rèn)我們下一次也能按時(shí)按量地償還貸款,只需要有一個(gè)擔(dān)保人能保證如果我不能還款時(shí),可以從他的賬戶扣錢,那銀行就認(rèn)為沒有風(fēng)險(xiǎn)了。

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

關(guān)于對(duì)新生代進(jìn)行分配擔(dān)保的內(nèi)容,在本章講解垃圾收集器執(zhí)行規(guī)則時(shí)還會(huì)再詳細(xì)講解。

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

復(fù)制算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi) 50% 的空間,就需要有額外的空間進(jìn)行內(nèi)存分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都是 100% 存活的極端情況,所以在老年代一般不能直接選用這種算法。

根據(jù)老年代的特點(diǎn),有人提出了另外一種“標(biāo)記-整理”(Mark-Compact)算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存,“標(biāo)記-整理”算法示意圖如下:

4. 分代收集算法

當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”算法—Generational Collection,根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊并采用不用的垃圾收集算法。

一般把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>

在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成垃圾收集。而老年代中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記—清理”或者“標(biāo)記—整理”算法來(lái)進(jìn)行回收。

HotSpot 的算法實(shí)現(xiàn)

1. 枚舉根節(jié)點(diǎn)

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

另外,可達(dá)性分析對(duì)執(zhí)行時(shí)間的敏感性還體現(xiàn)在 GC 停頓上,因?yàn)檫@項(xiàng)分析工作必須不可以出現(xiàn)分析過程中對(duì)象引用關(guān)系還在不斷變化的情況,否則分析結(jié)果準(zhǔn)確性就無(wú)法得到保證。這點(diǎn)是導(dǎo)致 GC 進(jìn)行時(shí)必須停頓所有運(yùn)行的 Java 執(zhí)行線程(Sun將這件事情稱為"Stop The World")的其中一個(gè)重要原因,即使是在號(hào)稱(幾乎)不會(huì)發(fā)生停頓的 CMS 收集器中,枚舉根節(jié)點(diǎn)時(shí)也是必須要停頓的。

因此,目前的主流 Java 虛擬機(jī)使用的都是準(zhǔn)確式 GC(即虛擬機(jī)可以知道內(nèi)存中某個(gè)位置的數(shù)據(jù)具體是什么類型。),所以當(dāng)執(zhí)行系統(tǒng)停頓下來(lái)后,并不需要一個(gè)不漏地檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得知哪些地方存放著對(duì)象引用。

在 HotSpot 的實(shí)現(xiàn)中,使用一組稱為 OopMap 的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到這個(gè)目的的,在類加載完成的時(shí)候,HotSpot 就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來(lái),在 JIT 的編譯過程中,也會(huì)在特定的位置記錄棧和寄存器中哪些位置是引用,因此 GC 在掃描時(shí)就可以直接得知這些信息了。

2. 安全點(diǎn)(Safepoint)

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

實(shí)際上,HotSpot 也的確沒有為每條指令都生成 OopMap,前面已經(jīng)提到,只是在特定的位置記錄了這些信息,這些位置稱為安全點(diǎn),即程序執(zhí)行時(shí)并非在所有地方都能停頓下來(lái)開始 GC ,只有在到達(dá)安全點(diǎn)時(shí)才能暫停。

Safepoint 的選定既不能太少以致于 GC 過少,也不能過于頻繁以致于過分增大運(yùn)行時(shí)的負(fù)荷。

對(duì)于 Safepoint,另一個(gè)需要考慮的問題是如何在 GC 發(fā)生時(shí)讓所有線程都“跑”到最近的安全點(diǎn)上再停頓下來(lái)。這里有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動(dòng)式中斷(Voluntary Suspension)。

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

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

3. 安全區(qū)域(Safe Region)

使用 Safepoint 似乎已經(jīng)完美地解決了如何進(jìn)入 GC 的問題,但實(shí)際情況卻并不一定。

Safepoint 機(jī)制保證了程序執(zhí)行時(shí),在不太長(zhǎng)的時(shí)間內(nèi)就會(huì)遇到可進(jìn)入 GC 的 Safepoint 安全點(diǎn),但是程序“不執(zhí)行”的時(shí)候呢?

所謂的程序不執(zhí)行就是沒有分配 CPU 時(shí)間,典型的例子就是線程處于 Sleep 狀態(tài)或 Blocked 狀態(tài),這時(shí)候線程無(wú)法響應(yīng) JVM 虛擬機(jī)的中斷請(qǐng)求,”走“到安全的地方去中斷掛起,JVM也顯然不太可能等待線程重新被分配 CPU 時(shí)間。對(duì)于這種情況,就需要安全區(qū)域(Safe Region)來(lái)解決。

安全區(qū)域是指在一段代碼片段之中,引用關(guān)系不會(huì)發(fā)生變化。

在這個(gè)區(qū)域中的任意地方開始 GC 都是安全的。我們也可以把 Safe Region 看做是被擴(kuò)展了的 Safepoint。在線程執(zhí)行到 Safe Region 中的代碼時(shí),首先標(biāo)識(shí)自己已經(jīng)進(jìn)入了 Safe Region,那樣,當(dāng)在這段時(shí)間里 JVM 要發(fā)起 GC 時(shí),就不用管標(biāo)識(shí)自己為 Safe Region 狀態(tài)的線程了。在線程要離開 Safe Region 時(shí),它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個(gè) GC 過程),如果完成了,那線程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開 Safe Region 的信號(hào)為止。

垃圾收集器

如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。這里討論的收集器基于JDK 1.7 Update 14之后的 HotSpot 虛擬機(jī),這個(gè)虛擬機(jī)包含的所有收集器如下圖所示

上圖展示了 7 種作用于不同分代的收集器,如果兩個(gè)收集器之間存在連線,就說明它們可以搭配使用。虛擬機(jī)所處的區(qū)域,則表示它是屬于新生代收集器還是老年代收集器。接下來(lái)將逐一介紹這些收集器的特性、基本原理和使用場(chǎng)景,并重點(diǎn)分析 CMS 和 G1 這兩款相對(duì)復(fù)雜的收集器,了解它們的部分運(yùn)作細(xì)節(jié)。

1. Serial收集器(串行收集器)

Serial 收集器是最基本、發(fā)展歷史最悠久的收集器,曾經(jīng)是虛擬機(jī)新生代收集的唯一選擇。這是一個(gè)單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會(huì)使用一個(gè) CPU 或一條收集線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到它收集結(jié)束。

"Stop The World"這個(gè)名字也許聽起來(lái)很酷,但這項(xiàng)工作實(shí)際上是由虛擬機(jī)在后臺(tái)自動(dòng)發(fā)起和自動(dòng)完成的,在用戶不可見的情況下把用戶正常工作的線程全部停掉,這對(duì)很多應(yīng)用來(lái)說都是難以接受的。下圖示意了 Serial/Serial Old 收集器的運(yùn)行過程。

實(shí)際上到現(xiàn)在為止,該收集器依然是虛擬機(jī)運(yùn)行在 Client 模式下的默認(rèn)新生代收集器。它也有著優(yōu)于其他收集器的地方:簡(jiǎn)單而高效(與其他收集器的單線程比),對(duì)于限定單個(gè) CPU 的環(huán)境來(lái)說,Serial 收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。

在用戶的桌面應(yīng)用場(chǎng)景中,分配給虛擬機(jī)管理的內(nèi)存一般來(lái)說不會(huì)很大,收集幾十兆甚至一兩百兆的新生代(僅僅是新生代使用的內(nèi)存,桌面應(yīng)用基本上不會(huì)再大了),停頓時(shí)間完全可以控制在幾十毫秒最多一百多毫秒以內(nèi),只要不是頻繁發(fā)生,這點(diǎn)停頓是可以接受的。所以,Serial 收集器對(duì)于運(yùn)行在 Client 模式下的虛擬機(jī)來(lái)說是一個(gè)很好的選擇。

2. ParNew收集器

ParNew 收集器其實(shí)就是 Serial 收集器的多線程版本,除了使用多條線程進(jìn)行垃圾收集之外,其余行為包括 Serial 收集器可用的所有控制參數(shù)(-XX:HandlePromotionFailure 以及設(shè)置閥值的-XX:PretenureSizeThreshold 、-XX:SurvivorRatio 等)、收集算法、Stop The World、對(duì)象分配規(guī)則、回收策略等都與 Serial 收集器完全一樣,在實(shí)現(xiàn)上,這兩種收集器也共用了相當(dāng)多的代碼。ParNew 收集器的工作過程如下圖所示:

ParNew 收集器除了多線程收集之外,其他與 Serial 收集器相比并沒有太多創(chuàng)新之處,但它卻是許多運(yùn)行在 Server 模式下的虛擬機(jī)中首選的新生代收集器,其中有一個(gè)與性能無(wú)關(guān)但很重要的原因是,除了 Serial 收集器外,目前只有它能與 CMS 收集器(并發(fā)收集器,后面有介紹)配合工作。

ParNew 收集器在單 CPU 的環(huán)境中不會(huì)有比 Serial 收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術(shù)實(shí)現(xiàn)的兩個(gè) CPU 的環(huán)境中都不能百分之百地保證可以超越 Serial 收集器。

當(dāng)然,隨著可以使用的 CPU 的數(shù)量的增加,它對(duì)于 GC 時(shí)系統(tǒng)資源的有效利用還是很有好處的。它默認(rèn)開啟的收集線程數(shù)與 CPU 的數(shù)量相同,在 CPU 非常多(如 32 個(gè))的環(huán)境下,可以使用 -XX:ParallelGCThreads 參數(shù)來(lái)限制垃圾收集的線程數(shù)。

注意,從 ParNew 收集器開始,后面還會(huì)接觸到幾款并發(fā)和并行的收集器。這里有必要先解釋兩個(gè)名詞:并發(fā)和并行。這兩個(gè)名詞都是并發(fā)編程中的概念,在談?wù)摾占鞯纳舷挛恼Z(yǔ)境中,它們可以解釋如下。

  • 并行(Parallel):指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍然處于等待狀態(tài)。
  • 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè) CPU 上。

3. Parallel Scanvenge收集器

Parallel Scavenge 收集器是一個(gè)新生代收集器,它也是使用復(fù)制算法的收集器,又是并行的多線程收集器……看上去和 ParNew 都一樣,那它有什么特別之處呢?

Parallel Scavenge 收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同,CMS 等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶線程的停頓時(shí)間,而 Parallel Scavenge 收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)。

所謂吞吐量就是 CPU 用于運(yùn)行用戶代碼的時(shí)間與 CPU 總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間),虛擬機(jī)總共運(yùn)行了 100 分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99% 。

停頓時(shí)間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能提升用戶體驗(yàn),而高吞吐量則可以高效率地利用 CPU 時(shí)間,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。

Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的 -XX:MaxGCPauseMillis 參數(shù)以及直接設(shè)置吞吐量大小的 -XX:GCTimeRatio 參數(shù)。

MaxGCPauseMillis 參數(shù)允許的值是一個(gè)大于 0 的毫秒數(shù),收集器將盡可能地保證內(nèi)存回收花費(fèi)的時(shí)間不超過設(shè)定值。

不過大家不要認(rèn)為如果把這個(gè)參數(shù)的值設(shè)置得稍小一點(diǎn)就能使得系統(tǒng)的垃圾收集速度變得更快,GC停頓時(shí)間縮短是以犧牲吞吐量和新生代空間來(lái)?yè)Q取的:系統(tǒng)把新生代調(diào)小一些,收集300MB 新生代肯定比收集 500MB 快吧,這也直接導(dǎo)致垃圾收集發(fā)生得更頻繁一些,原來(lái)10秒收集一次、每次停頓100毫秒,現(xiàn)在變成5秒收集一次、每次停頓70毫秒。停頓時(shí)間的確在下降,但吞吐量也降下來(lái)了。

GCTimeRatio 參數(shù)的值應(yīng)當(dāng)是一個(gè) 0 到 100 的整數(shù),也就是垃圾收集時(shí)間占總時(shí)間的比率,相當(dāng)于是吞吐量的倒數(shù)。如果把此參數(shù)設(shè)置為 19,那允許的最大 GC 時(shí)間就占總時(shí)間的 5%(即 1/(1+19)),默認(rèn)值為 99 ,就是允許最大 1%(即 1/(1+99))的垃圾收集時(shí)間。

由于與吞吐量關(guān)系密切,Parallel Scavenge 收集器也經(jīng)常稱為“吞吐量?jī)?yōu)先”收集器。除上述兩個(gè)參數(shù)之外,Parallel Scavenge 收集器還有一個(gè)參數(shù) -XX:+UseAdaptiveSizePolicy 值得關(guān)注。這是一個(gè)開關(guān)參數(shù),當(dāng)這個(gè)參數(shù)打開之后,就不需要手工指定新生代的大小參數(shù) -Xmn、Eden 與 Survivor 區(qū)的比例參數(shù) -XX:SurvivorRatio、晉升老年代對(duì)象的年齡閥值 -XX:PretenureSizeThreshold 等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大的吞吐量,這種調(diào)節(jié)方式稱為 GC 自適應(yīng)的調(diào)節(jié)策略(GC Ergonomics)。

4. Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,它同樣是一個(gè)單線程收集器,使用“標(biāo)記-整理”算法。這個(gè)收集器的主要意義也是在于給 Client 模式下的虛擬機(jī)使用。如果在 Server 模式下,那么它主要還有兩大用途:一種用途是在 JDK 1.5 以及之前的版本中與 Parallel Scavenge 收集器搭配使用,另一種用途就是作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時(shí)使用。這兩點(diǎn)都將在后面的內(nèi)容中詳細(xì)講解。Serial Old 收集器的工作過程如下圖所示:

5. Parellel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法。這個(gè)收集器是在 JDK 1.6 中才開始提供的,在此之前,新生代的 Parallel Scavenge 收集器一直處于比較尷尬的狀態(tài)。

原因是,如果新生代選擇了 Parallel Scavenge 收集器,老年代除了 Serial Old(PS MarkSweep)收集器外別無(wú)選擇(Parallel Scavenge 收集器無(wú)法與 CMS 收集器配合工作)。

由于老年代 Serial Old 收集器在服務(wù)端應(yīng)用性能上的“拖累”,使用了 Parallel Scavenge 收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果,由于單線程的老年代收集中無(wú)法充分利用服務(wù)器多 CPU 的處理能力,在老年代很大而且硬件比較高級(jí)的環(huán)境中,這種組合的吞吐量甚至還不一定有 ParNew 加 CMS 的組合“給力”。

直到 Parallel Old 收集器出現(xiàn)后,“吞吐量?jī)?yōu)先”收集器終于有了比較名副其實(shí)的應(yīng)用組合,在注重吞吐量以及 CPU 資源敏感的場(chǎng)合,都可以優(yōu)先考慮 Parallel Scavenge 加 Parallel Old 收集器。Parallel Old 收集器的工作過程如下圖所示:

6. CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。

目前很大一部分的 Java 應(yīng)用集中在互聯(lián)網(wǎng)站或者 B/S 系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來(lái)較好的體驗(yàn)。CMS 收集器就非常符合這類應(yīng)用的需求。

從名字(包含"Mark Sweep")上就可以看出,CMS 收集器是基于“標(biāo)記—清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來(lái)說更復(fù)雜一些,整個(gè)過程分為4個(gè)步驟,包括:

  • 初始標(biāo)記(CMS initial mark)
  • 并發(fā)標(biāo)記(CMS concurrent mark)
  • 重新標(biāo)記(CMS remark)
  • 并發(fā)清除(CMS concurrent sweep)

其中,初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要"Stop The World"。初始標(biāo)記僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象,速度很快,并發(fā)標(biāo)記階段就是進(jìn)行 GC RootsTracing 的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。

由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來(lái)說,CMS 收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。

CMS 是一款優(yōu)秀的收集器,它的主要優(yōu)點(diǎn)在名字上已經(jīng)體現(xiàn)出來(lái)了:并發(fā)收集、低停頓,但是 CMS 還遠(yuǎn)達(dá)不到完美的程度,它有以下 3 個(gè)明顯的缺點(diǎn):

第一、導(dǎo)致吞吐量降低。CMS 收集器對(duì) CPU 資源非常敏感。其實(shí),面向并發(fā)設(shè)計(jì)的程序都對(duì) CPU 資源比較敏感。在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)因?yàn)檎加昧艘徊糠志€程(或者說CPU資源)而導(dǎo)致應(yīng)用程序變慢,總吞吐量會(huì)降低。

CMS 默認(rèn)啟動(dòng)的回收線程數(shù)是(CPU數(shù)量+3)/4,也就是當(dāng) CPU 在4個(gè)以上時(shí),并發(fā)回收時(shí)垃圾收集線程不少于 25% 的 CPU 資源,并且隨著 CPU 數(shù)量的增加而下降。但是當(dāng) CPU 不足 4 個(gè)(譬如2個(gè))時(shí),CMS 對(duì)用戶程序的影響就可能變得很大,如果本來(lái) CPU 負(fù)載就比較大,還分出一半的運(yùn)算能力去執(zhí)行收集器線程,就可能導(dǎo)致用戶程序的執(zhí)行速度忽然降低了 50%,其實(shí)也讓人無(wú)法接受。

第二、CMS 收集器無(wú)法處理浮動(dòng)垃圾(Floating Garbage),可能出現(xiàn)"Concurrent Mode Failure"失敗而導(dǎo)致另一次 Full GC(新生代和老年代同時(shí)回收) 的產(chǎn)生。由于 CMS 并發(fā)清理階段用戶線程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過程之后,CMS 無(wú)法在當(dāng)次收集中處理掉它們,只好留待下一次 GC 時(shí)再清理掉。這一部分垃圾就稱為“浮動(dòng)垃圾”。

也是由于在垃圾收集階段用戶線程還需要運(yùn)行,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶線程使用,因此 CMS 收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。

在 JDK 1.5 的默認(rèn)設(shè)置下,CMS 收集器當(dāng)老年代使用了 68% 的空間后就會(huì)被激活,這是一個(gè)偏保守的設(shè)置,如果在應(yīng)用中老年代增長(zhǎng)不是太快,可以適當(dāng)調(diào)高參數(shù) -XX:CMSInitiatingOccupancyFraction 的值來(lái)提高觸發(fā)百分比,以便降低內(nèi)存回收次數(shù)從而獲取更好的性能,在 JDK 1.6 中,CMS 收集器的啟動(dòng)閾值已經(jīng)提升至 92% 。

要是 CMS 運(yùn)行期間預(yù)留的內(nèi)存無(wú)法滿足程序需要,就會(huì)出現(xiàn)一次"Concurrent Mode Failure"失敗,這時(shí)虛擬機(jī)將啟動(dòng)后備預(yù)案:臨時(shí)啟用 Serial Old 收集器來(lái)重新進(jìn)行老年代的垃圾收集,這樣停頓時(shí)間就很長(zhǎng)了,參數(shù) -XX:CM SInitiatingOccupancyFraction 設(shè)置得太高很容易導(dǎo)致大量"Concurrent Mode Failure"失敗,性能反而降低。

第三、產(chǎn)生空間碎片。CMS 是一款基于“標(biāo)記—清除”算法實(shí)現(xiàn)的收集器,這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生??臻g碎片過多時(shí),將會(huì)給大對(duì)象分配帶來(lái)很大麻煩,往往會(huì)出現(xiàn)老年代還有很大空間剩余,但是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象,不得不提前觸發(fā)一次 Full GC 。

為了解決這個(gè)問題,CMS 收集器提供了一個(gè) -XX:+UseCMSCompactAtFullCollection 開關(guān)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器頂不住要進(jìn)行 FullGC 時(shí)開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理的過程是無(wú)法并發(fā)的,空間碎片問題沒有了,但停頓時(shí)間不得不變長(zhǎng)。虛擬機(jī)設(shè)計(jì)者還提供了另外一個(gè)參數(shù) -XX:CMSFullGCsBeforeCompaction,這個(gè)參數(shù)是用于設(shè)置執(zhí)行多少次不壓縮的 Full GC 后,跟著來(lái)一次帶壓縮的(默認(rèn)值為0,表示每次進(jìn)入Full GC時(shí)都進(jìn)行碎片整理)。

7. G1 收集器

G1(Garbage-First)收集器是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一,從JDK 9 版本開始將 G1 變成默認(rèn)的垃圾收集器,它是一款面向服務(wù)端應(yīng)用的垃圾收集器,。HotSpot 開發(fā)團(tuán)隊(duì)賦予它的使命是(在比較長(zhǎng)期的)未來(lái)可以替換掉 JDK 1.5 中發(fā)布的 CMS 收集器。與其他 GC 收集器相比,G1 具備如下特點(diǎn)。

并行與并發(fā):G1 能充分利用多 CPU、多核環(huán)境下的硬件優(yōu)勢(shì),使用多個(gè)CPU(CPU或者CPU核心)來(lái)縮短 Stop-The-World 停頓的時(shí)間,部分其他收集器原本需要停頓 Java 線程執(zhí)行的 GC 動(dòng)作,G1 收集器仍然可以通過并發(fā)的方式讓 Java 程序繼續(xù)執(zhí)行。

分代收集: 與其他收集器一樣,分代概念在 G1 中依然得以保留。雖然 G1 可以不需要其他收集器配合就能獨(dú)立管理整個(gè) GC 堆,但它能夠采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過多次 GC 的舊對(duì)象以獲取更好的收集效果。

空間整合: 與 CMS 的“標(biāo)記—清理”算法不同,G1 從整體來(lái)看是基于“標(biāo)記—整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè) Region 之間)上來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的,但無(wú)論如何,這兩種算法都意味著 G1 運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o(wú)法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次 GC 。

可預(yù)測(cè)的停頓: 這是 G1 相對(duì)于 CMS 的另一大優(yōu)勢(shì),降低停頓時(shí)間是 G1 和 CMS 共同的關(guān)注點(diǎn),但 G1 除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒,這幾乎已經(jīng)是實(shí)時(shí) Java(RTSJ)的垃圾收集器的特征了。

在 G1 之前的其他收集器進(jìn)行收集的范圍都是整個(gè)新生代或者老年代,而 G1 不再是這樣。使用 G1 收集器時(shí),Java 堆的內(nèi)存布局就與其他收集器有很大差別,它將整個(gè) Java 堆劃分為多個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分 Region (不需要連續(xù))的集合。

G1 收集器之所以能建立可預(yù)測(cè)的停頓時(shí)間模型,是因?yàn)樗梢杂杏?jì)劃地避免在整個(gè)Java堆中進(jìn)行全區(qū)域的垃圾收集。G1 在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的 Region(這也就是Garbage-First名稱的來(lái)由),保證了 G1 收集器在有限的時(shí)間內(nèi)可以獲取盡可能高的收集效率。

在 G1 收集器中,Region 之間的對(duì)象引用以及其他收集器中的新生代與老年代之間的對(duì)象引用,虛擬機(jī)都是使用 Remembered Set 來(lái)避免全堆掃描的。

G1 中每個(gè)Region 都有一個(gè)與之對(duì)應(yīng)的 Remembered Set,虛擬機(jī)發(fā)現(xiàn)程序在對(duì) Reference 類型的數(shù)據(jù)進(jìn)行寫操作時(shí),會(huì)產(chǎn)生一個(gè) Write Barrier 暫時(shí)中斷寫操作,檢查 Reference 引用的對(duì)象是否處于不同的 Region 之中(在分代的例子中就是檢查是否老年代中的對(duì)象引用了新生代中的對(duì)象),如果是,便通過 CardTable 把相關(guān)引用信息記錄到被引用對(duì)象所屬的 Region 的 Remembered Set 之中。當(dāng)進(jìn)行內(nèi)存回收時(shí),在 GC 根節(jié)點(diǎn)的枚舉范圍中加入 Remembered Set 即可保證不對(duì)全堆掃描也不會(huì)有遺漏。

如果不計(jì)算維護(hù) Remembered Set 的操作,G1 收集器的運(yùn)作大致可劃分為以下幾個(gè)步驟:

  1. 初始標(biāo)記(Initial Marking)
  2. 并發(fā)標(biāo)記(Concurrent Marking)
  3. 最終標(biāo)記(Final Marking)
  4. 篩選回收(Live Data Counting and Evacuation)

G1 的前幾個(gè)步驟的運(yùn)作過程和 CMS 有很多相似之處。

初始標(biāo)記階段僅僅只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象,并且修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序并發(fā)運(yùn)行時(shí),能在正確可用的 Region 中創(chuàng)建新對(duì)象,這階段需要停頓線程,但耗時(shí)很短。

并發(fā)標(biāo)記階段是從 GC Root 開始對(duì)堆中對(duì)象進(jìn)行可達(dá)性分析,找出存活的對(duì)象,這階段耗時(shí)較長(zhǎng),但可與用戶程序并發(fā)執(zhí)行。

而最終標(biāo)記階段則是為了修正在并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分標(biāo)記記錄,虛擬機(jī)將這段時(shí)間對(duì)象變化記錄在線程 Remembered Set Logs 里面,最終標(biāo)記階段需要把 Remembered Set Logs 的數(shù)據(jù)合并到 Remembered Set 中,這階段需要停頓線程,但是可并行執(zhí)行。

最后在篩選回收階段首先對(duì)各個(gè) Region 的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的 GC 停頓時(shí)間來(lái)制定回收計(jì)劃,從Sun公司透露出來(lái)的信息來(lái)看,這個(gè)階段其實(shí)也可以做到與用戶程序一起并發(fā)執(zhí)行,但是因?yàn)橹换厥找徊糠? Region,時(shí)間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。通過下圖可以比較清楚地看到G1收集器的運(yùn)作步驟中并發(fā)和需要停頓的階段。

GC 日志

閱讀 GC 日志是處理 Java 虛擬機(jī)內(nèi)存問題的基礎(chǔ)技能,它只是一些人為確定的規(guī)則,沒有太多技術(shù)含量。

每一種收集器的日志形式都是由它們自身的實(shí)現(xiàn)所決定的,換而言之,每個(gè)收集器的日志格式都可以不一樣。但虛擬機(jī)設(shè)計(jì)者為了方便用戶閱讀,將各個(gè)收集器的日志都維持一定的共性,例如以下兩段典型的 GC 日志:

  1. 33.125:[GC[DefNew:3324K->152K(3712K),0.0025925 secs]3324K->152K(11904K),0.0031680 secs] 
  2. 100.667:[Full GC[Tenured:0 K->210K(10240K),0.0149142secs]4603K->210K(19456K),[Perm:2999K->2999K(21248K)],0.0150007 secs][Times:user=0.01 sys=0.00,real=0.02 secs] 

最前面的數(shù)字33.125: 和 100.667: 代表了 GC 發(fā)生的時(shí)間,這個(gè)數(shù)字的含義是從 Java 虛擬機(jī)啟動(dòng)以來(lái)經(jīng)過的秒數(shù)。

GC 日志開頭的 [GC 和 [Full GC 說明了這次垃圾收集的停頓類型,而不是用來(lái)區(qū)分新生代 GC 還是老年代 GC 的。

如果有 Full ,說明這次 GC 是發(fā)生了 Stop-The-World 的,例如下面這段新生代收集器 ParNew 的日志也會(huì)出現(xiàn) [Full GC(這一般是因?yàn)槌霈F(xiàn)了分配擔(dān)保失敗之類的問題,所以才導(dǎo)致 STW)。如果是調(diào)用 System.gc() 方法所觸發(fā)的收集,那么在這里將顯示 [Full GC(System)。

  1. [Full GC 283.736:[ParNew:261599K->261599K(261952K),0.0000288 secs] 

接下來(lái)的 [DefNew、[Tenured、[Perm 表示 GC 發(fā)生的區(qū)域,這里顯示的區(qū)域名稱與使用的 GC 收集器是密切相關(guān)的,例如上面樣例所使用的 Serial 收集器中的新生代名為 "Default New Generation",所以顯示的是 [DefNew。如果是 ParNew 收集器,新生代名稱就會(huì)變?yōu)?[ParNew,意為 "Parallel New Generation"。如果采用 Parallel Scavenge 收集器,那它配套的新生代稱為 PSYoungGen,老年代和永久代同理,名稱也是由收集器決定的。

后面方括號(hào)內(nèi)部的 3324K->152K(3712K) 含義是GC 前該內(nèi)存區(qū)域已使用容量 -> GC 后該內(nèi)存區(qū)域已使用容量 (該內(nèi)存區(qū)域總?cè)萘?。而在方括號(hào)之外的 3324K->152K(11904K) 表示 GC 前 Java 堆已使用容量 ->GC 后 Java 堆已使用容量 (Java 堆總?cè)萘?。

再往后,0.0025925 secs 表示該內(nèi)存區(qū)域 GC 所占用的時(shí)間,單位是秒。有的收集器會(huì)給出更具體的時(shí)間數(shù)據(jù),如 [Times:user=0.01 sys=0.00,real=0.02 secs] ,這里面的 user、sys 和 real 與 Linux 的 time 命令所輸出的時(shí)間含義一致,分別代表用戶態(tài)消耗的 CPU 時(shí)間、內(nèi)核態(tài)消耗的 CPU 事件和操作從開始到結(jié)束所經(jīng)過的墻鐘時(shí)間(Wall Clock Time)。

CPU 時(shí)間與墻鐘時(shí)間的區(qū)別是,墻鐘時(shí)間包括各種非運(yùn)算的等待耗時(shí),例如等待磁盤 I/O、等待線程阻塞,而 CPU 時(shí)間不包括這些耗時(shí),但當(dāng)系統(tǒng)有多 CPU 或者多核的話,多線程操作會(huì)疊加這些 CPU 時(shí)間,所以讀者看到 user 或 sys 時(shí)間超過 real 時(shí)間是完全正常的。

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

JDK 1.7 中的各種垃圾收集器到此已全部介紹完畢,在描述過程中提到了很多虛擬機(jī)非穩(wěn)定的運(yùn)行參數(shù),在表3-2中整理了這些參數(shù)供讀者實(shí)踐時(shí)參考:

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

對(duì)象的內(nèi)存分配,往大方向講,就是在堆上分配,對(duì)象主要分配在新生代的Eden區(qū)上。少數(shù)情況下也可能會(huì)直接分配在老年代中,分配的規(guī)則并不是百分之百固定的,其細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集器組合,還有虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)的設(shè)置。

圖摘自《碼出高效》

1. 對(duì)象優(yōu)先在 Eden 分配

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

虛擬機(jī)提供了 -XX:+PrintGCDetails 這個(gè)收集器日志參數(shù),告訴虛擬機(jī)在發(fā)生垃圾收集行為時(shí)打印內(nèi)存回收日志,并且在進(jìn)程退出的時(shí)候輸出當(dāng)前的內(nèi)存各區(qū)域分配情況。

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

運(yùn)行結(jié)果:

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

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

執(zhí)行 testAllocation() 中分配 allocation4 對(duì)象的語(yǔ)句時(shí)會(huì)發(fā)生一次 Minor GC,這次 GC 的結(jié)果是新生代 6651KB 變?yōu)?148KB ,而總內(nèi)存占用量則幾乎沒有減少(因?yàn)?allocation1、allocation2、allocation3 三個(gè)對(duì)象都是存活的,虛擬機(jī)幾乎沒有找到可回收的對(duì)象)。

這次 GC 發(fā)生的原因是給 allocation4 分配內(nèi)存的時(shí)候,發(fā)現(xiàn) Eden 已經(jīng)被占用了 6MB,剩余空間已不足以分配 allocation4 所需的 4MB 內(nèi)存,因此發(fā)生 Minor GC。GC 期間虛擬機(jī)又發(fā)現(xiàn)已有的 3 個(gè) 2MB 大小的對(duì)象全部無(wú)法放入 Survivor 空間(Survivor 空間只有 1MB 大小),所以只好通過分配擔(dān)保機(jī)制提前轉(zhuǎn)移到老年代去。

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

2. Minor GC 與 Full GC 的區(qū)別

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

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

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

所謂的大對(duì)象是指,需要大量連續(xù)內(nèi)存空間的 Java 對(duì)象,最典型的大對(duì)象就是那種很長(zhǎng)的字符串以及數(shù)組( byte[] 數(shù)組就是典型的大對(duì)象)。大對(duì)象對(duì)虛擬機(jī)的內(nèi)存分配來(lái)說就是一個(gè)壞消息(特別是短命大對(duì)象,寫程序的時(shí)候應(yīng)當(dāng)避免),經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來(lái)“安置”它們。

虛擬機(jī)提供了 -XX:PretenureSizeThreshold 參數(shù),令大于這個(gè)設(shè)置值的對(duì)象直接在老年代分配。這樣做的目的是避免在 Eden 區(qū)及兩個(gè) Survivor 區(qū)之間發(fā)生大量的內(nèi)存復(fù)制。

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

運(yùn)行結(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.

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

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

4. 長(zhǎng)期存活的對(duì)象將進(jìn)入老年代

虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器。

如果對(duì)象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然存活,并且能被 Survivor 容納的話,將被移動(dòng)到 Survivor 空間中,并且對(duì)象年齡設(shè)為 1 。對(duì)象在 Survivor 區(qū)中每“熬過”一次 Minor GC,年齡就增加 1 歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲),就將會(huì)被晉升到老年代中。

對(duì)象晉升老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold 設(shè)置。

5. 動(dòng)態(tài)對(duì)象年齡判定

為了能更好地適應(yīng)不同程序的內(nèi)存狀況,無(wú)須等到 MaxTenuringThreshold 中要求的年齡,同年對(duì)象達(dá)到 Survivor 空間的一半后,他們以及年齡大于他們的對(duì)象都將直接進(jìn)入老年代。

6. 空間分配擔(dān)保

在發(fā)生 Minor GC 之前,虛擬機(jī)會(huì)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果這個(gè)條件成立,那么 Minor GC 可以確保是安全的。

只要老年代的連續(xù)空間大于新生代對(duì)象總大小或者歷次晉升的平均大小就會(huì)進(jìn)行 Minor GC ,否則將進(jìn)行 Full GC 。

 

責(zé)任編輯:武曉燕 來(lái)源: 碼農(nóng)私房話
相關(guān)推薦

2020-05-14 13:39:19

Java 垃圾回收機(jī)制

2009-06-23 14:15:00

Java垃圾回收

2023-08-27 21:29:43

JVMFullGC調(diào)優(yōu)

2023-06-07 16:00:40

JavaScriptV8語(yǔ)言

2011-07-04 16:48:56

JAVA垃圾回收機(jī)制GC

2015-06-04 09:38:39

Java垃圾回收機(jī)

2011-06-28 12:39:34

Java垃圾回收

2017-08-17 15:40:08

大數(shù)據(jù)Python垃圾回收機(jī)制

2010-09-25 15:33:19

JVM垃圾回收

2017-03-03 09:26:48

PHP垃圾回收機(jī)制

2010-10-13 10:24:38

垃圾回收機(jī)制JVMJava

2021-11-05 15:23:20

JVM回收算法

2010-09-16 15:10:24

JVM垃圾回收機(jī)制

2010-09-25 15:26:12

JVM垃圾回收

2021-05-27 21:47:12

Python垃圾回收

2024-02-22 17:15:22

JS垃圾回收機(jī)制

2017-06-12 17:38:32

Python垃圾回收引用

2010-09-26 14:08:41

Java垃圾回收

2016-08-11 14:26:29

Java垃圾回收機(jī)制內(nèi)存分配

2016-08-11 15:02:54

Java垃圾回收機(jī)制內(nèi)存
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)