JVM性能調優(2) —— 垃圾回收器和回收策略

一、垃圾回收機制

1、為什麼需要垃圾回收

Java 程序在虛擬機中運行,是會佔用內存資源的,比如創建的對象、加載的類型數據等,而且內存資源都是有限的。當創建的對象不再被引用時,就需要被回收掉,釋放內存資源,這個時候就會用到JVM的垃圾回收機制。

JVM 啟動時就提供了一個垃圾回收線程來跟蹤每一塊分配出去的內存空間,並定期清理需要被回收的對象。Java 程序無法強制執行垃圾回收,我們可以通過調用 System.gc 方法來”建議”執行垃圾回收,但是否可執行,什麼時候執行,是不可預期的。

2、垃圾回收發生在哪裡

JVM內存模型中,程序計數器、虛擬機棧、本地方法棧這 3 個區域是線程私有的,隨着線程的創建而創建,銷毀而銷毀。棧中的棧幀隨着方法的調用而入棧,隨着方法的退出而出棧,每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的。因此這三個區域的內存分配和回收都具有確定性。

而堆和方法區這兩個區域則有着顯著的不確定性:一個接口的多個實現類需要的內存可能會不一樣,一個方法所執行的不同條件分支所需要的內存也可能不一樣,只有處於運行期間,才能知道程序究竟會創建哪些對象,創建多少個對象,這部分內存的分配和回收是動態的。垃圾回收的重點就是關注堆和方法區中的內存,堆中的回收主要是垃圾對象的回收,方法區的回收主要是廢棄常量和無用的類的回收。

3、對象在什麼時候可以被回收 

一般一個對象不再被引用,就代表該對象可以被回收。主流的虛擬機一般都是使用 可達性分析算法 來判斷該對象是否可以被回收,有些內存管理系統也是用 引用計數法 來判斷。

1)引用計數算法:

這種算法是通過在對象中添加一個引用計數器來判斷該對象是否被引用了。每當對象被引用,計數器就加 1;每當引用失效,計數器就減 1。當對象的引用計數器的值為 0 時,就說明該對象不再被引用,可以被回收了。

引用計數算法實現簡單,判斷效率也很高,但它無法解決對象之間相互循環引用的問題。兩個對象若互相引用,但沒有任何其它對象引用他們,而它們的引用計數器都不為零,就無法被回收。

2)可達性分析算法:

GC  Roots  是該算法的基礎,GC Roots 是所有對象的根對象。在垃圾回收時,會從這些 GC Roots 根對象開始向下搜索,在搜索的這個引用鏈上的對象,就是可達的對象;而一個對象到 GC Roots 沒有任何引用鏈相連時,就證明此對象是不可達的,可以被回收。

在Java中,可作為 GC Roots 對象的一般包括如下幾種:

  • Java虛擬機棧中的引用的對象,如方法參數、局部變量、臨時變量等 
  • 方法區中的類靜態屬性引用的對象 
  • 方法區中的常量引用的對象,如字符串常量池的引用 
  • 本地方法棧中JNI的引用的對象
  • Java虛擬機內部的引用,如基本數據類型的 Class 對象,系統類加載器等

比如下面的代碼:

其中,類靜態變量 MAPPER,loadAccount 方法的局部變量 account1、account2、accountList 都可以作為 GC Roots(ArrayList 內部是用 Object[] elementData 數組來存放元素的)。

在調用 loadAccount 方法時,堆中的對象都是可達的,因為有 GC Roots 直接或間接引用到這些對象,此時若發生垃圾回收,這些對象是不可被回收的。loadAccount 執行完後,彈出棧幀,方法內的局部變量都被回收了,雖然堆中 ArrayList 對象還指向 elementData 數組,而 elementData 指向 Account 對象,但沒有任何 GC Roots 的引用鏈能達到這些對象,因此這些對象將變為垃圾對象,被垃圾回收器回收掉。

4、回收方法區

方法區垃圾回收的「性價比」通常是比較低的,方法區的垃圾回收主要回收兩部分內容:廢棄的常量和不再使用的類型。

1)廢棄的常量:

  • 如常量池中廢棄的字面量,字段、方法的符號引用等

2)不再使用的類型:

判定一個類型是否屬於「不再被使用的類」需要同時滿足三個條件:

  • 該類所有的實例都已經被回收,Java堆中不存在該類及其任何派生子類的實例
  • 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

在大量使用反射、動態代理、CGLib等位元組碼框架,動態生成JSP以及OSGi這類頻繁自定義類加載器的場景中,通常都需要Java虛擬機具備類型卸載的能力,以保證不會對方法區造成過大的內存壓力。

5、Java中的引用類型

Java 中有四種不同的引用類型:強引用、軟引用、弱引用、虛引用,這4種引用強度依次逐漸減弱。

1)強引用:

強引用是最普遍的引用方式,如在方法中定義:Object obj = new Object()。只要引用還在,垃圾回收器就不會回收被引用的對象。

2)軟引用:

軟引用是用來描述一些有用但非必須的對象,可以使用 SoftReference 類來實現軟引用。對於軟引用關聯着的對象,在系統將要發生內存溢出異常之前(一般發生老年代GC時),會把這些對象列進回收範圍之中。如果回收之後內存還是不足,才會報內存溢出的異常。

這一點可以很好地用來解決OOM的問題,並且這個特性很適合用來實現內存緩存,當內存快滿時,就回收掉這些軟引用的對象,然後需要的時候再重新查詢。比如下面的代碼:

3)弱引用:

弱引用是用來描述非必須的對象,可以使用 WeakReference 類來實現弱引用。它只能生存到下一次垃圾回收發生之前(一般發生年輕代GC時),當垃圾回收機制開始時,無論是否會內存溢出,都將回收掉被弱引用關聯的對象。

需注意的是,我們使用 SoftReference 來創建軟引用對象,使用 WeakReference 來創建弱引用對象,垃圾回收時,是回收它們關聯的對象,而不是 Reference 本身。同時,如果 Reference 關聯的對象被其它 GC Roots 引用着,也是不能被回收的。如下面的代碼,在垃圾回收時,只有 T002 這個 Account 對象能被回收,回收後 reference2.get() 返回值為 null,account、reference1、reference2 所指向的對象都不能被回收。

4)虛引用:

最沒有存在感的一種引用關係,可以使用 PhantomReference 類來實現虛引用。存在不存在幾乎沒影響,也不能通過虛引用來獲取一個對象實例,存在的唯一目的是被垃圾回收器回收後可以收到一條系統通知。

二、垃圾回收算法

1、分代收集理論

大部分虛擬機的垃圾回收器都是遵循「分代收集」的理論進行設計的,它的核心思想是根據對象存活的生命周期將內存劃分為若干個不同的區域。一般至少將堆劃分為新生代和老年代兩個區域,然後可以根據不同代的特點採取最適合的回收算法。在新生代中,每次垃圾回收時都有大量對象死去,因為程序創建的絕大部分對象的生命周期都很短,朝生夕滅。而新生代每次回收後存活的少量對象,將會逐步晉陞到老年代中存放。老年代每次垃圾收集時只有少量對象需要被回收,因為老年代的大部分對象一般都是全局變量引用的,生命周期一般都比較長。

在Java堆劃分出不同的區域之後,垃圾回收器就可以每次只回收其中某一個或者某些部分的區域,因而也有了「Young GC」、「Old GC」、「Full GC」這樣的回收類型的劃分。也能夠針對不同的區域安排與裏面存儲對象存亡特徵相匹配的垃圾回收算法,因而發展出了「標記-複製算法」、「標記-清除算法」、「標記-整理算法」等針對性的垃圾回收算法。

GC類型:

  • 新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
  • 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。
  • 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
  • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集,包括新生代、老年代、方法區的回收,一般 Full GC 等價於 Old GC。

經典分代模型:

2、標記-清除算法(Mark-Sweep)

標記-清除算法 分為「標記」和「清除」兩個階段,首先從 GC Roots 進行掃描,對存活的對象進行標記,標記完後,再統一回收所有未被標記的對象。

優點:

  • 標記-清除算法不需要進行對象的移動,只需回收未標記的垃圾對象,在存活對象比較多的情況下極為高效。

缺點:

  • 標記-清除算法執行效率不穩定,如果堆中對象很多,而且大部分都是要回收的對象,就必須要進行大量的標記和清除動作,導致標記、清除兩個過程的效率隨着對象數量增長而降低。
  • 標記、清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致當以後在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

3、標記-複製算法(Copying)

標記-複製算法簡稱為複製算法,複製算法主要是為了解決標記-清除算法在存在大量可回收對象時執行效率低下和內存碎片的問題。

1)半區複製算法

它將可用內存劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存滿了,就從 GC Roots 開始掃描,將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

優點:

  • 每次都是針對整個半區進行內存回收,清理速度快,沒有內存碎片產生
  • 每次回收後,對象有序排列到另一個空閑區域,分配內存時也就不用考慮有空間碎片的複雜情況

缺點:

  • 如果內存中多數對象都是存活的,這種算法將會產生大量的內存間複製的開銷
  • 複製回收算法將可用內存縮小為了原來的一半,內存使用率低

2)複製算法的優化

大多數對象都是朝生夕滅,新生代中98%的對象幾乎都熬不過第一輪迴收,因此並不需要按照 1∶1 的比例來劃分新生代的內存空間。

因此新生代複製算法一般是把新生代分為一塊較大的 Eden 區和兩塊較小的 Survivor(survivor0、survivor1) 區,每次分配內存只使用 Eden 和其中一塊 Survivor。發生垃圾回收時,將 Eden 和 Survivor 中仍然存活的對象一次性複製到另外一塊 Survivor 空間上,然後直接清理掉 Eden 和已用過的那塊 Survivor 空間,如此往複。當對象經過垃圾回收的次數超過一定閥值還未被回收掉時,就會進入老年代,有些大對象也可以直接進入老年代。

相比半區複製算法:

優點:HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8 : 1 : 1,新生代與老年代的比例大概是 1 : 2。內存空間利用率高,只會有 10% 的空閑空間。

缺點:有可能一次 Young GC 後存活的對象超過一個 survivor 區的大小,這時候會依賴其它內存區域進行分配擔保,讓這部分存活下來的對象直接進入另一個區域,一般就是老年代。

4、標記-整理算法(Mark-Compact)

複製算法在對象存活率較高時就要進行較多的複製操作,效率將會降低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。

標記-整理算法採用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,它不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存。

優點:沒有內存碎片產生,適合老年代垃圾回收

缺點:會有對象的移動,老年代存活對象多,移動對象還需要更新指針,因此成本會更高

5、總結對比

三、垃圾回收器

垃圾回收算法是內存回收的方法論,垃圾回收器是內存回收的實踐者。不同的垃圾回收器有不同的特性,並沒有一個萬能或最好的垃圾回收器,只能根據不同的業務場景選擇最合適的垃圾回收器,所以這節就來了解下各個垃圾回收器的特性。

1、Stop The World(STW)

先看看jvm的 「Stop The World」 問題。

1)STW:

可達性分析算法從 GC Roots 集合找引用鏈時,需要枚舉根節點,然後從根節點標記存活的對象,根節點枚舉以及整理內存碎片時,都會發生 Stop The World,此時 jvm 會直接暫停應用程序的所有用戶線程,然後進行垃圾回收。因為垃圾回收時如果還在繼續創建對象或更新對象引用,就會導致這些對象可能無法跟蹤和回收、跟節點不斷變化等比較複雜的問題,因此垃圾回收過程必須暫停所有用戶線程,進入 STW 狀態。垃圾回收完成後,jvm 會恢復應用程序的所有用戶線程。

所有垃圾回收器都無法避免 STW,只能盡量縮短用戶線程的停頓時間。系統停頓期間,無法處理任何請求,所有用戶請求都會出現短暫的卡頓。如果因為內存分配不合理或垃圾回收器使用不合理,導致頻繁的垃圾回收,而且每次回收系統停頓時間過長,這會讓用戶體驗極差。jvm 最重要的一個優化就是通過合理的內存分配,使用合適的垃圾回收器,使得垃圾回收頻率最小、停頓時間最短,避免影響系統正常運行。

2)安全點(Safe Point):

用戶程序執行時並非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點後才能夠暫停。安全點可以理解成是在代碼執行過程中的一些特殊位置,當線程執行到這些位置的時候,說明虛擬機當前的狀態是安全的,如果有需要,可以在這個位置暫停。

安全點位置的選取基本上是以「是否具有讓程序長時間執行的特徵」為標準進行選定的,「長時間執行」的最明顯特徵就是指令序列的復用,例如方法調用、循環跳轉、異常跳轉等都屬於指令序列復用,所以只有具有這些功能的指令才會產生安全點。

jvm 採用主動式中斷的方式,在垃圾回收發生時讓所有線程都跑到最近的安全點。主動式中斷的思想是當垃圾回收需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌位,各個線程執行過程時會不停地主動去輪詢這個標誌,一旦發現中斷標誌為真時就自己在最近的安全點上主動中斷掛起。

3)安全區域(Safe Region):

安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入垃圾回收過程的安全點。但是,程序「不執行」的時候,線程就無法響應虛擬機的中斷請求,如用戶線程處於Sleep狀態或者Blocked狀態,這個時候就沒法再走到安全的地方去中斷掛起自己。這就需要安全區域來解決了。

安全區域是指能夠確保在某一段代碼片段之中,引用關係不會發生變化,因此,在這個區域中任意地方開始垃圾回收都是安全的。當用戶線程執行到安全區域裏面的代碼時,首先會標識自己已經進入了安全區域,那樣當這段時間裏虛擬機要發起垃圾回收時就不必去管這些已聲明自己在安全區域內的線程了。當線程要離開安全區域時,它要檢查虛擬機是否已經完成了需要暫停用戶線程的階段,如果完成了,那線程就繼續執行;否則它就必須一直等待,直到收到可以離開安全區域的信號為止。

2、Serial 垃圾回收器

Serial 垃圾回收器是一個單線程回收器,它進行垃圾回收時,必須暫停其他所有用戶線程,直到它回收結束。Serial 主要用於新生代垃圾回收,採用複製算法實現。

服務端程序幾乎不會使用 Serial 回收器,服務端程序一般會分配較大的內存,可能幾個G,如果使用 Serial 回收器,由於是單線程,標記、清理階段就會花費很長的時間,就會導致系統較長時間的停頓。

Serial 一般用在客戶端程序或佔用內存較小的微服務,因為客戶端程序一般分配的內存都比較小,可能幾十兆或一兩百兆,回收時的停頓時間是完全可以接受的。而且 Serial 是所有回收器里額外消耗內存最小的,也沒有線程切換的開銷,非常簡單高效。

3、Serial Old 垃圾回收器

Serial Old 是 Serial 的老年代版本,它同樣是一個單線程回收器,主要用於客戶端程序。Serial Old 用於老年代垃圾回收,採用標記-整理算法實現。

Serial Old 也可以用在服務端程序,主要有兩種用途:一種是與 Parallel Scavenge 回收器搭配使用,另外一種就是作為 CMS 回收器發生失敗時的後備預案,在並發收集發生 Concurrent Mode Failure 時使用。

4、ParNew 垃圾回收器

ParNew 回收器實質上是 Serial 回收器的多線程並行版本,除了同時使用多條線程進行垃圾收集之外,其餘的行為都與 Serial 回收完全一致,控制參數、回收算法、對象分配規則等都是一致的。除了 Serial 回收器外,目前只有 ParNew 回收器能與 CMS 回收器配合工作,ParNew 是激活CMS後的默認新生代回收器。

ParNew 默認開啟的回收線程數與處理器核心數量相同,在處理器核心非常多的環境中,可以使用 -XX: ParallelGCThreads 參數來限制垃圾回收的線程數。

5、Parallel Scavenge 垃圾回收器

Parallel Scavenge是新生代回收器,採用複製算法實現,也是能夠並行回收的多線程回收器。Parallel Scavenge 主要關注可控制的吞吐量,其它回收器的關注點是儘可能地縮短垃圾回收時的停頓時間。吞吐量就是處理器用於運行程序代碼的時間與處理器總消耗時間的比值,總消耗時間等於運行程序代碼的時間加上垃圾回收的時間。

Parallel Scavenge 提供了兩個參數用於精確控制吞吐量:

  • -XX: MaxGCPauseMillis:控制最大垃圾回收停頓時間,參數值是一個大於0的毫秒數,回收器將儘力保證垃圾回收花費的時間不超過這個值。
  • -XX: GCTimeRatio:直接設置吞吐量大小,參數值是一個大於0小於100的整數,就是垃圾回收時間佔總時間的比率。默認值為 99,即允許最大1%(即1/(1+99))的垃圾收集時間。

Parallel Scavenge 還有一個參數 -XX: +UseAdaptiveSizePolicy,當設置這個參數之後,就不需要人工指定新生代的大小、Eden與Survivor區的比例等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

6、Parallel Old 垃圾回收器

Parallel Old 是 Parallel Scavenge 的老年代版本,支持多線程並發回收,採用標記-整理算法實現。在注重吞吐量或者處理器資源較為稀缺的場合,可以優先考慮 Parallel Scavenge 加 Parallel Old 這個組合。

7、CMS 垃圾回收器

CMS(Concurrent Mark Sweep)是一種以獲取最短回收停頓時間為目標的回收器。CMS 用於老年代垃圾回收,採用標記-清除算法實現。

1)CMS 回收過程:

CMS 垃圾回收整個過程分為四個步驟:

  • 1)初始標記:初始標記需要 Stop The World,初始標記僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快。
  • 2)並發標記:並發標記階段就是從 GC Roots 的直接關聯對象開始遍歷整個對象引用鏈的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾回收線程一起並發運行。
  • 3)重新標記:重新標記需要 Stop The World,重新標記階段是為了修正並發標記期間,因程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短。
  • 4)並發清除:清除階段是清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時並發進行的。

最耗時的並發標記和並發清除階段是和用戶線程並發進行的,總體上來說,CMS 回收過程是與用戶線程一起並發執行的,是一款並發低停頓的回收器。

2)CMS 的問題:

① 並發回收導致CPU資源緊張:

在並發階段,它雖然不會導致用戶線程停頓,但卻會因為佔用了一部分線程而導致應用程序變慢,降低程序總吞吐量。CMS默認啟動的回收線程數是:(CPU核數 + 3)/ 4,當CPU核數不足四個時,CMS對用戶程序的影響就可能變得很大。

② 無法清理浮動垃圾:

在CMS的並發標記和並發清理階段,用戶線程還在繼續運行,就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以後,CMS無法在當次收集中處理掉它們,只好留到下一次垃圾收集時再清理掉。這一部分垃圾稱為「浮動垃圾」。

③ 並發失敗(Concurrent Mode Failure):

由於在垃圾回收階段用戶線程還在並發運行,那就還需要預留足夠的內存空間提供給用戶線程使用,因此CMS不能像其他回收器那樣等到老年代幾乎完全被填滿了再進行回收,必須預留一部分空間供並發回收時的程序運行使用。默認情況下,當老年代使用了 92% 的空間後就會觸發 CMS 垃圾回收,這個值可以通過 -XX: CMSInitiatingOccupancyFraction 參數來設置。

這裡會有一個風險:要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次「並發失敗」(Concurrent Mode Failure),這時候虛擬機將不得不啟動後備預案:Stop The World,臨時啟用 Serial Old 來重新進行老年代的垃圾回收,這樣一來停頓時間就很長了。所以參數 -XX: CMSInitiatingOccupancyFraction 設置得太高將會很容易導致大量的並發失敗產生,性能反而降低;太低又可能頻繁觸發CMS回收,所以在生產環境中應根據實際應用情況來權衡設置。

-XX: CMSInitiatingOccupancyFraction 參數值默認為-1,計算出來的閥值是92%,也可以自己指定值,同時還需要設置 -XX:+UseCMSInitiatingOccupancyOnly,讓JVM使用設定的回收閾值,如果不設置,JVM僅在第一次使用設定值,後續則自動調整。

而且,CMS並不是時時刻刻都在執行GC的,可以通過 -XX:CMSWaitDuration 參數設置CMS GC線程的間隔時間,默認值為2000毫秒。

④ 內存碎片問題:

CMS是一款基於「標記-清除」算法實現的回收器,這意味着回收結束時會有內存碎片產生。內存碎片過多時,將會給大對象分配帶來麻煩,往往會出現老年代還有很多剩餘空間,但就是無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次 Full GC 的情況。

為了解決這個問題,CMS收集器提供了一個 -XX:+UseCMSCompactAtFullCollection 開關參數(默認開啟),用於在 Full GC 時開啟內存碎片的合併整理過程,由於這個內存整理必須移動存活對象,是無法並發的,這樣停頓時間就會變長。還有另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數的作用是要求CMS在執行過若干次不整理空間的 Full GC 之後,下一次進入 Full GC 前會先進行碎片整理(默認值為0,表示每次進入 Full GC 時都進行碎片整理)。

8、G1 垃圾回收器

G1(Garbage First)回收器採用面向局部收集的設計思路和基於Region的內存布局形式,是一款主要面向服務端應用的垃圾回收器。G1設計初衷就是替換 CMS,成為一種全功能收集器。G1 在JDK9 之後成為服務端模式下的默認垃圾回收器,取代了 Parallel Scavenge 加 Parallel Old 的默認組合,而 CMS 被聲明為不推薦使用的垃圾回收器。G1從整體來看是基於 標記-整理 算法實現的回收器,但從局部(兩個Region之間)上看又是基於 標記-複製 算法實現的。

1)G1 回收器的特點:

② 可預期的回收停頓時間

G1 可以指定垃圾回收的停頓時間,通過 -XX: MaxGCPauseMillis 參數指定,默認為 200 毫秒。這個值不宜設置過低,否則會導致每次回收只佔堆內存很小的一部分,回收器的回收速度逐漸趕不上對象分配速度,導致垃圾慢慢堆積,最終佔滿堆內存導致 Full GC 反而降低性能。

G1之所以能建立可預測的停頓時間模型,是因為它將 Region 作為單次回收的最小單元,即每次回收到的內存空間都是 Region 大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾回收。G1會去跟蹤各個Region的垃圾回收價值,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後台維護一個優先級列表,每次根據用戶設定允許的回收停頓時間,優先處理回收價值收益最大的那些Region。這種使用Region劃分內存空間,以及具有優先級的區域回收方式,保證了G1回收器在有限的時間內得到儘可能高的回收效率。

③ 佔用更高的內存

由於Region數量比傳統回收器的分代數量明顯要多得多,因此G1回收器要比其他的傳統垃圾回收器有着更高的內存佔用負擔。G1至少要耗費大約相當於Java堆容量10%至20%的額外內存來維持回收器工作。

2)G1內存布局

G1不再是固定大小以及固定數量的分代區域劃分,而是把堆劃分為多個大小相等的Region,每個Region的大小默認情況下是堆內存大小除以2048,因為JVM最多可以有2048個Region,而且每個Region的大小必須是2的N次冥。每個Region的大小也可以通過參數 -XX:G1HeapRegionSize 設定,取值範圍為1MB~32MB,且應為2的N次冪。

G1也有新生代和老年代的概念,不過是邏輯上的區分,每一個 Region 都可以根據需要,作為新生代的Eden空間、Survivor空間,或者老年代空間。新生代默認占堆內存的5%,但最多不超過60%,這個最大值可以通過 -XX:G1MaxNewSizePercent 參數設置。

3)大對象Region

Region中還有一類特殊的 Humongous 區域,專門用來存儲大對象,而不是直接進入老年代的Region。G1認為一個對象只要大小超過了一個Region容量的一半就判定為大對象。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的 Humongous Region 之中,G1的大多數行為都把 Humongous Region 作為老年代的一部分來看待。

3)G1 回收過程

G1 回收器的運作過程大致可分為四個步驟:

  • 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的對象,並且修改TAMS指針的值,讓下一階段用戶線程並發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際並沒有額外的停頓。
  • 並發標記:從 GC Roots 開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序並發執行。當對象圖掃描完成以後,還要重新處理在並發時有引用變動的對象。
  • 最終標記:對用戶線程做短暫的暫停,處理並發階段結束後仍有引用變動的對象。
  • 混合回收:更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整箇舊Region的全部空間。這裡的操作涉及存活對象的移動,必須暫停用戶線程,由多條回收器線程並行完成的。

4)G1混合回收

G1有一個參數,-XX:InitiatingHeapOccupancyPercent,它的默認值是45%,就是如果老年代占堆內存45%的Region的時候,此時就會觸發一次年輕代+老年代的混合回收。

混合回收階段,因為我們設定了最大停頓時間,所以 G1 會從新生代、老年代、大對象里挑選一些 Region,保證指定的時間內回收儘可能多的垃圾。所以 G1 可能一次無法將所有Region回收完,它就會執行多次混合回收,先停止程序,執行一次混合回收回收掉一些Region,接着恢復系統運行,然後再次停止系統運行,再執行一次混合回收回收掉一些Region。可以通過參數 -XX:G1MixedGCCountTarget 設置一次回收的過程中,最後一個階段最多執行幾次混合回收,默認值是8次。通過這種反覆回收的方式,避免系統長時間的停頓。

G1還有一個參數 -XX:G1HeapWastePercent,默認值是 5%。就是在混合回收時,Region回收後,就會不斷的有新的Region空出來,一旦空閑出來的Region數量超過堆內存的5%,就會立即停止混合回收,即本次混合回收就結束了。

G1還有一個參數 -XX:G1MixedGCLiveThresholdPercent,默認值是85%。意思是必須要回收Region的時候,必須存活對象低於Region大小的85%時才可以進行回收,一個Region存活對象超過85%,就不必回收它了,因為要複製大部分存活對象到別的Region,這個成本是比較高的。

5)回收失敗

① 並發回收失敗

在並發標記階段,用戶線程還在並發運行,程序繼續運行就會持續有新對象產生,也需要預留足夠的空間提供給用戶線程使用。G1為每一個Region設計了兩個名為TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用於並發回收過程中的新對象分配,並發回收時新分配的對象地址都必須要在這兩個指針位置以上。G1默認在這個地址以上的對象是被隱式標記過的,即默認它們是存活的,不納入回收範圍。如果內存回收的速度趕不上內存分配的速度,跟CMS會發生並發失敗一樣,G1也要被迫暫停程序,導致 Full GC 而產生長時間 Stop The World。

② 混合回收失敗

混合回收階段,年輕代和老年代都是基於複製算法進行回收,複製的過程中如果沒有空閑的Region了,就會觸發失敗。一旦失敗,就會停止程序,然後採用單線程標記、清理和內存碎片整理,然後空閑出來一批Region。這個過程是很慢的,因此要盡量調優避免混合回收失敗的發生。

9、總結對比

1)垃圾回收器間的配合使用

2)各個垃圾回收器對比

10、GC性能衡量指標

一個垃圾收集器在不同場景下表現出的性能也不一樣,我們可以藉助下面的一些指標來衡量GC的性能。

1)吞吐量

吞吐量是指應用程序所花費的時間和系統總運行時間的比值。系統總運行時間 = 應用程序耗時 +GC 耗時。如果系統運行了 100 分鐘,GC 耗時 1 分鐘,則系統吞吐量為 99%。GC 的吞吐量一般不能低於 95%。

2)停頓時間

指垃圾收集器正在運行時,應用程序的暫停時間。對於串行回收器而言,停頓時間可能會比較長;而使用並發回收器,由於垃圾收集器和應用程序交替運行,程序的停頓時間就會變短,但其效率很可能不如獨佔垃圾收集器,系統的吞吐量也很可能會降低。

3)垃圾回收頻率

通常垃圾回收的頻率越低越好,增大堆內存空間可以有效降低垃圾回收發生的頻率,但同時也意味着堆積的回收對象越多,最終也會增加回收時的停頓時間。所以我們只要適當地增大堆內存空間,保證正常的垃圾回收頻率即可。

四、內存設置和查看GC日誌

1、設置JVM內存

1)JVM內存分配有如下一些參數:

  • -Xms:堆內存大小
  • -Xmx:堆內存最大大小
  • -Xmn:新生代大小,扣除新生代剩下的就是老年代大小
  • -Xss:線程棧大小
  • -XX:NewSize:初始新生代大小
  • -XX:MaxNewSize:最大新生代大小
  • -XX:InitialHeapSize:初始堆大小
  • -XX:MaxHeapSize:最大堆大小
  • -XX:MetaspaceSize:元空間(永久代)大小,jdk1.8 之前用 -XX:PermSize 設置
  • -XX:MaxMetaspaceSize:元空間(永久代)最大大小,jdk8 之前用 -XX:MaxPermSize 設置
  • -XX:SurvivorRatio:新生代 Eden 區和 Survivor 區的比例,默認為 8,即 8:1:1

一般 -Xms 和 -Xmx 設置一樣的大小,-XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 設置一樣的大小。-Xms 等價於 -XX:InitialHeapSize,-Xmx等價於-XX:MaxHeapSize;-Xmn等價於-XX:MaxNewSize。

2)在IDEA中可以按照如下方式設置JVM參數:

3)命令行啟動時可以按照如下格式設置:

java -jar -Xms1G -Xmx1G -Xmn512M -Xss1M -XX:MetaspaceSize=128M -XX:MaxMetaspaceSize=128M app.jar

2、查看GC日誌

1)設置GC參數:

可以在啟動時加上如下參數來查看GC日誌:

  • -XX:+PrintGC:打印GC日誌
  • -XX:+PrintGCDetails:打印詳細的GC日誌
  • -XX:+PrintGCTimeStamps:打印每次GC發生的時間
  • -Xloggc:./gc.log:設置GC日誌文件的路徑

例如,我在IDEA中添加了如下JVM啟動參數:

-Xms1G
-Xmx1G
-Xmn512M
-Xss1M
-XX:MetaspaceSize=128M
-XX:MaxMetaspaceSize=128M
-XX:SurvivorRatio=8
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log

啟動程序之後打印出了如下的一些日誌:

 1 Java HotSpot(TM) 64-Bit Server VM (25.191-b12) for windows-amd64 JRE (1.8.0_191-b12), built on Oct  6 2018 09:29:03 by "java_re" with MS VC++ 10.0 (VS2010)
 2 Memory: 4k page, physical 33408872k(22219844k free), swap 35506024k(21336808k free)
 3 CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:CompressedClassSpaceSize=125829120 -XX:InitialHeapSize=1073741824 -XX:+ManagementServer -XX:MaxHeapSize=1073741824 -XX:MaxMetaspaceSize=134217728 -XX:MaxNewSize=536870912 -XX:MetaspaceSize=134217728 -XX:NewSize=536870912 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:ThreadStackSize=1024 -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
 4 2020-09-25T13:00:41.631+0800: 4.013: [GC (Allocation Failure) [PSYoungGen: 419840K->20541K(472064K)] 419840K->20573K(996352K), 0.0118345 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
 5 2020-09-25T13:00:44.252+0800: 6.633: [GC (Allocation Failure) [PSYoungGen: 440381K->39872K(472064K)] 440413K->39928K(996352K), 0.0180292 secs] [Times: user=0.08 sys=0.08, real=0.02 secs] 
 6 2020-09-25T13:00:45.509+0800: 7.891: [GC (Allocation Failure) [PSYoungGen: 459712K->45102K(472064K)] 459768K->45174K(996352K), 0.0181544 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
 7 2020-09-25T13:00:46.809+0800: 9.191: [GC (Allocation Failure) [PSYoungGen: 464942K->48670K(472064K)] 465014K->48785K(996352K), 0.0214228 secs] [Times: user=0.16 sys=0.00, real=0.02 secs] 
 8 2020-09-25T13:00:48.425+0800: 10.807: [GC (Allocation Failure) [PSYoungGen: 468510K->52207K(472064K)] 468625K->57076K(996352K), 0.0218655 secs] [Times: user=0.17 sys=0.00, real=0.02 secs] 
 9 ......
10 ......
11 2020-09-25T13:06:58.361+0800: 380.743: [GC (Allocation Failure) [PSYoungGen: 422656K->14159K(472064K)] 610503K->204082K(996352K), 0.0111278 secs] [Times: user=0.16 sys=0.00, real=0.01 secs] 
12 Heap
13  PSYoungGen      total 472064K, used 406352K [0x00000000e0000000, 0x0000000100000000, 0x0000000100000000)
14   eden space 419840K, 93% used [0x00000000e0000000,0x00000000f7f00528,0x00000000f9a00000)
15   from space 52224K, 27% used [0x00000000f9a00000,0x00000000fa7d3d70,0x00000000fcd00000)
16   to   space 52224K, 0% used [0x00000000fcd00000,0x00000000fcd00000,0x0000000100000000)
17  ParOldGen       total 524288K, used 189923K [0x00000000c0000000, 0x00000000e0000000, 0x00000000e0000000)
18   object space 524288K, 36% used [0x00000000c0000000,0x00000000cb978d08,0x00000000e0000000)
19  Metaspace       used 111852K, capacity 117676K, committed 117888K, reserved 1153024K
20   class space    used 13876K, capacity 14914K, committed 14976K, reserved 1048576K

從第三行 CommandLine flags 可以得到如下的信息:

  • -XX:InitialHeapSize=1073741824:初始堆大小為1G(等於 -Xms 設置的值)
  • -XX:MaxHeapSize=1073741824:最大堆內存 1G(等於 -Xmx 設置的值)
  • -XX:NewSize=536870912:新生代初始大小 512M(等於 -Xmn 設置的值)
  • -XX:MaxNewSize=536870912:最新生代初始大小 512M(等於 -Xmn 設置的值)
  • -XX:MetaspaceSize=134217728:元空間大小 128M
  • -XX:MaxMetaspaceSize=134217728:最大元空間 128M
  • -XX:SurvivorRatio=8:新生代 Eden 和 Survivor 的比例
  • -XX:ThreadStackSize=1024:線程棧的大小 1M
  • -XX:+UseParallelGC:默認使用 年輕代 Parallel Scavenge + 老年代 Parallel Old 的垃圾回收器組合。

2)查看默認參數:

如果要查看JVM的默認參數,就可以通過給JVM加打印GC日誌的參數,就可以在GC日誌中看到JVM的默認參數了。

還可以在啟動參數中添加 -XX:+PrintFlagsFinal 參數,將會打印系統的所有參數,就可以看到自己配置的參數或系統的默認參數了:

3)GC日誌:

之後的日誌就是每次垃圾回收時產生的日誌,每行日誌說明了這次GC的執行情況,例如第四行GC日誌:

2020-09-25T13:00:41.631+0800: 4.013: [GC (Allocation Failure) [PSYoungGen: 419840K->20541K(472064K)] 419840K->20573K(996352K), 0.0118345 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

詳細內容如下:

  • 2020-09-25T13:00:41.631+0800:GC發生的時間點。
  • 4.013:系統運行多久之後發生的GC,單位秒,這裡就是系統運行 4.013 秒後發生了一次GC。
  • GC (Allocation Failure):說明了觸發GC的原因,這裡是指對象分配失敗導致的GC。
  • PSYoungGen:指觸發的是年輕代的垃圾回收,使用的是 Parallel Scavenge 垃圾回收器。
  • 419840K->20541K:對年輕代執行了一次GC,GC之前年輕代使用了 419840K,GC之後有 20541K 的對象活下來了。
  • (472064K):年輕代可用空間是 472064K,即 461 M,為什麼是461M呢?因為新生代大小為 512M,Eden 區占 409.6M,兩塊 Survivor 區各占 51.2M,所以年輕代的可用空間為 Eden+1個Survivor的大小,即460.8M,約為461M。
  • 419840K->20573K:GC前整個堆內存使用了 419840K,GC之後堆內存使用了 20573K。
  • (996352K):整個堆的大小是 996352K,即 973M,其實就是年輕代的 461M + 老年代的 512 M
  • 0.0118345 secs:本次GC耗費的時間
  • Times: user=0.00 sys=0.00, real=0.01 secs:本次GC耗費的時間

4)JVM退出時的GC情況:

程序結束運行後,還會打印一些日誌,就是第12行之後的日誌,這部分展示的是當前堆內存的使用情況:

1 Heap
2  PSYoungGen      total 472064K, used 406352K [0x00000000e0000000, 0x0000000100000000, 0x0000000100000000)
3   eden space 419840K, 93% used [0x00000000e0000000,0x00000000f7f00528,0x00000000f9a00000)
4   from space 52224K, 27% used [0x00000000f9a00000,0x00000000fa7d3d70,0x00000000fcd00000)
5   to   space 52224K, 0% used [0x00000000fcd00000,0x00000000fcd00000,0x0000000100000000)
6  ParOldGen       total 524288K, used 189923K [0x00000000c0000000, 0x00000000e0000000, 0x00000000e0000000)
7   object space 524288K, 36% used [0x00000000c0000000,0x00000000cb978d08,0x00000000e0000000)
8  Metaspace       used 111852K, capacity 117676K, committed 117888K, reserved 1153024K
9   class space    used 13876K, capacity 14914K, committed 14976K, reserved 1048576K

詳細內容如下:

  • PSYoungGen total 472064K, used 406352K:指 Parallel Scavenge 回收器負責的年輕代總共有 472064K(461M)內存,目前使用了 406352K (396.8M)。
  • eden space 419840K, 93% used:Eden 區的空間為 419840K(410M),已經使用了 93%。
  • from space 52224K, 27% used:From Survivor 區的空間為 52224K(51M),已經使用了 27%。
  • to space 52224K, 0% used:To Survivor 區的空間為 52224K(51M),使用了 0%,就是完全空閑的。
  • ParOldGen total 524288K, used 189923K:指 Parallel Old 回收器負責的老年代總共有 524288K(512M),目前使用了 189923K(185.4M)。
  • object space 524288K, 36% used:老年代空間總大小 524288K(512M),使用了 36%。
  • Metaspace & class space:Metaspace 元數據空間和Class空間,總容量、使用的內存等。

五、內存分配與回收策略

接下來我們就通過一些demo結合著GC日誌分析下什麼時候會觸發GC,以及對象在堆中如何分配流轉的。

1、對象首先分配到Eden區

我們通過如下這段程序來驗證下對象首先是分配到 Eden 區的:

1 public class GCMain {
2     static final int _1M = 1024 * 1024;
3 
4     public static void main(String[] args) {
5         byte[] b1 = new byte[_1M * 30];
6         byte[] b2 = new byte[_1M * 30];
7     }
8 }

jvm參數設置為如下:堆200M,年輕代 100M,Eden區占 80M,Survivor 各占 10M,老年代100M。使用默認的 Parallel Scavenge + Parallel Old 回收器。

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

程序運行結束後,查看運行的GC日誌:

 1 Java HotSpot(TM) 64-Bit Server VM (25.191-b12) for windows-amd64 JRE (1.8.0_191-b12), built on Oct  6 2018 09:29:03 by "java_re" with MS VC++ 10.0 (VS2010)
 2 Memory: 4k page, physical 33408872k(23013048k free), swap 35506024k(22095152k free)
 3 CommandLine flags: -XX:InitialHeapSize=209715200 -XX:MaxHeapSize=209715200 -XX:MaxNewSize=104857600 -XX:NewSize=104857600 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
 4 Heap
 5  PSYoungGen      total 92160K, used 68062K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
 6   eden space 81920K, 83% used [0x00000000f9c00000,0x00000000fde77ba0,0x00000000fec00000)
 7   from space 10240K, 0% used [0x00000000ff600000,0x00000000ff600000,0x0000000100000000)
 8   to   space 10240K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff600000)
 9  ParOldGen       total 102400K, used 0K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
10   object space 102400K, 0% used [0x00000000f3800000,0x00000000f3800000,0x00000000f9c00000)
11  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
12   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

從第5行可以看出,年輕代總共可用空間為 92160K(90M),已經使用了 68062K(66.4M)。代碼中創建了兩個30M的byte數組,為何會佔用66.4M呢?多出來的這部分對象可以認為是對象數組本身額外需要佔用的內存空間以及程序運行時所創建的一些額外的對象,就稱為未知對象吧。

從第6行之後可以看出,Eden 使用了 83%,From Survivor、To Survivor、老年代使用率均為 0%。可以確認對象首先是分配到 Eden 區的。

2、Eden 區滿了觸發 Minior GC

使用如下jvm參數:

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:+UseParallelGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

運行如下代碼,第2、3、4行將產生60M的垃圾對象,第6行再分配時,eden 區空間就不夠分配了,此時就會觸發一次 YoungGC:

1 public static void main(String[] args) {
2     byte[] b1 = new byte[_1M * 30];
3     b1 = new byte[_1M * 30];
4     b1 = null;
5 
6     byte[] b2 = new byte[_1M * 30];
7 }

看GC日誌可以發現觸發了一次 Young GC:

 1 2020-09-26T00:14:16.832+0800: 0.194: [GC (Allocation Failure) [PSYoungGen: 66424K->815K(92160K)] 66424K->823K(194560K), 0.0010813 secs] [Times: user=0.08 sys=0.08, real=0.00 secs] 
 2 Heap
 3  PSYoungGen      total 92160K, used 33993K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
 4   eden space 81920K, 40% used [0x00000000f9c00000,0x00000000fbc66800,0x00000000fec00000)
 5   from space 10240K, 7% used [0x00000000fec00000,0x00000000feccbca0,0x00000000ff600000)
 6   to   space 10240K, 0% used [0x00000000ff600000,0x00000000ff600000,0x0000000100000000)
 7  ParOldGen       total 102400K, used 8K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
 8   object space 102400K, 0% used [0x00000000f3800000,0x00000000f3802000,0x00000000f9c00000)
 9  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
10   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

第1行可以看出,由於內存分配失敗觸發了一次YoungGC,回收前內存佔用 66424K,回收後只有 815K 了。整個堆回收前佔用 66424K,回收後存活 823K。

第3行可以看出,程序結束後,新生代使用了 33993K,其中就包括最後一個 b2 對象。

第5行可以看出,Young GC後,存活的  815K 對象進入了 from survivor 區,佔用 7% 的空間。

從上面的分析可以確認 eden 區快滿了,無法給新生對象分配內存時,將觸發一次 Young GC,並把存活的對象複製到一個 survivor 區中。

3、大對象將直接進入老年代

要控制大對象的閥值可以通過 -XX:PretenureSizeThreshold 參數設置,但是它只對 Serial 和 ParNew 回收器生效,對 Parallel Scavenge 不生效,所以這裡我們使用 ParNew + CMS 的回收器組合,並設置大對象閥值為4M:

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=40M -XX:+UseConcMarkSweepGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

運行如下的代碼,直接創建一個 40M 的對象

1 public static void main(String[] args) {
2     byte[] b1 = new byte[_1M * 40];
3 }

查看GC日誌:

 1 Java HotSpot(TM) 64-Bit Server VM (25.191-b12) for windows-amd64 JRE (1.8.0_191-b12), built on Oct  6 2018 09:29:03 by "java_re" with MS VC++ 10.0 (VS2010)
 2 Memory: 4k page, physical 33408872k(22977952k free), swap 35506024k(21515696k free)
 3 CommandLine flags: -XX:InitialHeapSize=209715200 -XX:MaxHeapSize=209715200 -XX:MaxNewSize=104857600 -XX:NewSize=104857600 -XX:OldPLABSize=16 -XX:PretenureSizeThreshold=41943040 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseConcMarkSweepGC -XX:-UseLargePagesIndividualAllocation -XX:+UseParNewGC 
 4 Heap
 5  par new generation   total 92160K, used 6622K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
 6   eden space 81920K,   8% used [0x00000000f3800000, 0x00000000f3e77b80, 0x00000000f8800000)
 7   from space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
 8   to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 9  concurrent mark-sweep generation total 102400K, used 40960K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
10  Metaspace       used 3046K, capacity 4556K, committed 4864K, reserved 1056768K
11   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

第3、5、9行可以看出, -XX:+UseConcMarkSweepGC 參數默認啟用的是 ParNew + CMS 的回收器。

第5、6行可以看出,eden 區還是會有6M左右的未知對象。

第9行可以看出,CMS 負責的老年代內存大小為 102400K(100M),使用了 40960K(40M),就是代碼中創建的 b1 對象。

因此可以確認,超過 -XX:PretenureSizeThreshold 參數設置的大對象將直接進入老年代。

4、長期存活的對象將進入老年代

對象誕生在eden區中,eden區滿了之後,就會觸發YoungGC,將eden區存活的對象複製到survivor中,此時對象的GC年齡設為1歲。對象每熬過一次GC,GC年齡就增加1歲,當它超過一定閥值的時候就會被晉陞到老年代。GC年齡的閥值可以通過參數 -XX:MaxTenuringThreshold 設置,默認為 15。

設置如下JVM參數:eden 區80M,survivor 各佔10M,GC年齡閥值為2。

-Xms200M -Xmx200M -Xmn100M -XX:MaxTenuringThreshold=2 -XX:PretenureSizeThreshold=40M -XX:+UseConcMarkSweepGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

首先運行如下代碼,第4、5、6行將在eden區產生70M的垃圾對象,第8行再創建一個35M的對象時,eden區空間不足,將觸發第一次YoungGC:

 1 public static void main(String[] args) {
 2     byte[] b1 = new byte[_1M * 2]; // b1 為長期存活對象 占 2M
 3 
 4     byte[] b2 = new byte[_1M * 35];
 5     b2 = new byte[_1M * 35];
 6     b2 = null;
 7 
 8     byte[] b3 = new byte[_1M * 35];
 9     //b3 = new byte[_1M * 35];
10     //b3 = null;
11     //
12     //byte[] b4 = new byte[_1M * 35];
13     //b4 = new byte[_1M * 35];
14     //b4 = null;
15     //
16     //byte[] bx = new byte[_1M * 2];
17     //byte[] b5 = new byte[_1M * 35];
18 }

查看GC日誌:

1 2020-09-25T23:47:20.648+0800: 0.198: [GC (Allocation Failure) 2020-09-25T23:47:20.648+0800: 0.198: [ParNew: 78712K->2769K(92160K), 0.0013440 secs] 78712K->2769K(194560K), 0.0014923 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2 Heap
3  par new generation   total 92160K, used 41067K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
4   eden space 81920K,  46% used [0x00000000f3800000, 0x00000000f5d66800, 0x00000000f8800000)
5   from space 10240K,  27% used [0x00000000f9200000, 0x00000000f94b4600, 0x00000000f9c00000)
6   to   space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
7  concurrent mark-sweep generation total 102400K, used 0K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
8  Metaspace       used 3047K, capacity 4556K, committed 4864K, reserved 1056768K
9   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

可以看出,第一次YoungGC後還存活2769K的對象,然後複製到 from survivor 區,占 27% 的空間大小,包含2M的b1對象+700K左右的未知對象。此時 b1 對象GC年齡為1。

再運行如下代碼,同理,運行到第12行時,將觸發第二次 YoungGC:

 1 public static void main(String[] args) {
 2     byte[] b1 = new byte[_1M * 2]; // b1 為長期存活對象 占 2M
 3 
 4     byte[] b2 = new byte[_1M * 35];
 5     b2 = new byte[_1M * 35];
 6     b2 = null;
 7 
 8     byte[] b3 = new byte[_1M * 35];
 9     b3 = new byte[_1M * 35];
10     b3 = null;
11 
12     byte[] b4 = new byte[_1M * 35];
13     //b4 = new byte[_1M * 35];
14     //b4 = null;
15     //
16     //byte[] bx = new byte[_1M * 2];
17     //byte[] b5 = new byte[_1M * 35];
18 }

查看GC日誌:

 1 2020-09-25T23:53:57.325+0800: 0.196: [GC (Allocation Failure) 2020-09-25T23:53:57.325+0800: 0.196: [ParNew: 78712K->2770K(92160K), 0.0014935 secs] 78712K->2770K(194560K), 0.0016180 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 2 2020-09-25T23:53:57.331+0800: 0.201: [GC (Allocation Failure) 2020-09-25T23:53:57.331+0800: 0.201: [ParNew: 77693K->2888K(92160K), 0.0013393 secs] 77693K->2888K(194560K), 0.0013890 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 3 Heap
 4  par new generation   total 92160K, used 40367K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
 5   eden space 81920K,  45% used [0x00000000f3800000, 0x00000000f5c99b30, 0x00000000f8800000)
 6   from space 10240K,  28% used [0x00000000f8800000, 0x00000000f8ad2130, 0x00000000f9200000)
 7   to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 8  concurrent mark-sweep generation total 102400K, used 0K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
 9  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
10   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

可以看出,第二次YoungGC後,還存活2888K對象,此時複製到另一塊 survivor 區,占 28% 的內存,包含2M的b1對象+700K左右的未知對象。此時 b2 對象GC年齡加1,為2。

再運行如下代碼,第17行將觸發第三次YoungGC:

 1 public static void main(String[] args) {
 2     byte[] b1 = new byte[_1M * 2]; // b1 為長期存活對象 占 2M
 3 
 4     byte[] b2 = new byte[_1M * 35];
 5     b2 = new byte[_1M * 35];
 6     b2 = null;
 7 
 8     byte[] b3 = new byte[_1M * 35];
 9     b3 = new byte[_1M * 35];
10     b3 = null;
11 
12     byte[] b4 = new byte[_1M * 35];
13     b4 = new byte[_1M * 35];
14     b4 = null;
15 
16     byte[] bx = new byte[_1M * 2];
17     byte[] b5 = new byte[_1M * 35];
18 }

 

查看GC日誌:

 1 2020-09-26T00:00:39.242+0800: 0.188: [GC (Allocation Failure) 2020-09-26T00:00:39.243+0800: 0.188: [ParNew: 78712K->2749K(92160K), 0.0012472 secs] 78712K->2749K(194560K), 0.0013625 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 2 2020-09-26T00:00:39.247+0800: 0.193: [GC (Allocation Failure) 2020-09-26T00:00:39.247+0800: 0.193: [ParNew: 77672K->2867K(92160K), 0.0013000 secs] 77672K->2867K(194560K), 0.0013396 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 3 2020-09-26T00:00:39.252+0800: 0.197: [GC (Allocation Failure) 2020-09-26T00:00:39.252+0800: 0.197: [ParNew: 78732K->2048K(92160K), 0.0031018 secs] 78732K->4716K(194560K), 0.0031488 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
 4 Heap
 5  par new generation   total 92160K, used 38707K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
 6   eden space 81920K,  44% used [0x00000000f3800000, 0x00000000f5bcce50, 0x00000000f8800000)
 7   from space 10240K,  20% used [0x00000000f9200000, 0x00000000f9400010, 0x00000000f9c00000)
 8   to   space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
 9  concurrent mark-sweep generation total 102400K, used 2668K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
10  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
11   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

可以看出,第三次YoungGC後,年輕代還存活 2048K(2M),其實就是 bx 這個對象,bx 被複制到 from survivor 區,from survivor 佔用剛好20%(2M)。而此時老年代使用了 2668K,就是2M的 b1 對象+600K左右的未知對象。

所以可以判斷出,第三次YoungGC時,在要複製 eden區+from survivor 區的存活對象時,發現 survivor 區存活對象的GC年齡已經超過設置的閥值了,這時就會將超過閥值的對象複製到老年代。

5、動態對象年齡判斷

動態對象年齡判斷是指,在複製前,如果 survivior 區域內年齡1+年齡2+年齡3+…+年齡n的對象總和大於survivor區的50%時,年齡n及以上的對象就會進入老年代,不一定要達到15歲。

設置如下JVM參數:

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 -XX:PretenureSizeThreshold=40M -XX:+UseConcMarkSweepGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

運行如下代碼:

 1 public static void main(String[] args) {
 2     byte[] x1 = new byte[_1M * 3];
 3 
 4     byte[] b1 = new byte[_1M * 30];
 5     b1 = new byte[_1M * 30];
 6     b1 = null;
 7 
 8     byte[] b2 = new byte[_1M * 30]; // 觸發一次GC
 9 
10     byte[] x2 = new byte[_1M * 2];
11 
12     b2 = new byte[_1M * 30];
13     b2 = null;
14 
15     byte[] b3 = new byte[_1M * 30]; // 觸發一次GC
16 
17     //byte[] x3 = new byte[_1M];
18     //
19     //b3 = new byte[_1M * 30];
20     //b3 = null;
21     //
22     //byte[] b4 = new byte[_1M * 30]; // 觸發一次GC
23 }

查看GC日誌:

 1 2020-09-26T00:50:51.099+0800: 0.211: [GC (Allocation Failure) 2020-09-26T00:50:51.099+0800: 0.211: [ParNew: 69496K->3787K(92160K), 0.0020708 secs] 69496K->3787K(194560K), 0.0021864 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 2 2020-09-26T00:50:51.104+0800: 0.217: [GC (Allocation Failure) 2020-09-26T00:50:51.104+0800: 0.217: [ParNew: 70513K->6007K(92160K), 0.0030657 secs] 70513K->6007K(194560K), 0.0031105 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 3 Heap
 4  par new generation   total 92160K, used 38366K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
 5   eden space 81920K,  39% used [0x00000000f3800000, 0x00000000f5799b30, 0x00000000f8800000)
 6   from space 10240K,  58% used [0x00000000f8800000, 0x00000000f8dddf18, 0x00000000f9200000)
 7   to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 8  concurrent mark-sweep generation total 102400K, used 0K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
 9  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
10   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

結合代碼和GC日誌可以分析出,代碼運行到第15行之後,觸發了兩次GC,這時 x1 的GC年齡為2,x2的GC年齡為1。from survivor 佔了 58%了,從這裡也可以看出是在複製前來判斷動態年齡規則的。

再運行如下代碼:

 1 public static void main(String[] args) {
 2     byte[] x1 = new byte[_1M * 3];
 3 
 4     byte[] b1 = new byte[_1M * 30];
 5     b1 = new byte[_1M * 30];
 6     b1 = null;
 7 
 8     byte[] b2 = new byte[_1M * 30]; // 觸發一次GC
 9 
10     byte[] x2 = new byte[_1M * 2];
11 
12     b2 = new byte[_1M * 30];
13     b2 = null;
14 
15     byte[] b3 = new byte[_1M * 30]; // 觸發一次GC
16 
17     byte[] x3 = new byte[_1M];
18 
19     b3 = new byte[_1M * 30];
20     b3 = null;
21 
22     byte[] b4 = new byte[_1M * 30]; // 觸發一次GC
23 }

查看GC日誌:

 1 2020-09-26T00:57:03.279+0800: 0.197: [GC (Allocation Failure) 2020-09-26T00:57:03.279+0800: 0.197: [ParNew: 69496K->3785K(92160K), 0.0020626 secs] 69496K->3785K(194560K), 0.0021906 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 2 2020-09-26T00:57:03.285+0800: 0.203: [GC (Allocation Failure) 2020-09-26T00:57:03.285+0800: 0.203: [ParNew: 70511K->5980K(92160K), 0.0028174 secs] 70511K->5980K(194560K), 0.0028673 secs] [Times: user=0.16 sys=0.00, real=0.00 secs] 
 3 2020-09-26T00:57:03.290+0800: 0.208: [GC (Allocation Failure) 2020-09-26T00:57:03.290+0800: 0.208: [ParNew: 70832K->3072K(92160K), 0.0031929 secs] 70832K->6764K(194560K), 0.0032401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 4 Heap
 5  par new generation   total 92160K, used 34611K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
 6   eden space 81920K,  38% used [0x00000000f3800000, 0x00000000f56cce50, 0x00000000f8800000)
 7   from space 10240K,  30% used [0x00000000f9200000, 0x00000000f9500020, 0x00000000f9c00000)
 8   to   space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
 9  concurrent mark-sweep generation total 102400K, used 3692K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
10  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
11   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

結合代碼和GC日誌可以分析出,代碼運行到第22行時,觸發了第三次GC,第三次GC複製時,survivor 中年齡為1的 x2 對象(2M)+年齡為2的 x1 對象(3M)+ 年齡為2的未知對象 已經超過 survivor 的50%了,此時就觸發動態年齡判定規則,將年齡為2以上的對象晉陞到老年代。

從第7行看出,survivor 區還有30%(3M)的對象,就是回收後還存活的 x2(2M)+ x3(1M)。

從第9行看出,老年代使用了 3692K(3.6M),說明 x1(3M)+ 未知對象(500K左右)通過動態年齡判斷晉陞到老年代了。

6、無法放入Survivor區直接進入老年代

YoungGC時,如果eden區+ from survivor 區存活的對象無法放到 to survivor 區了,這個時候會直接將部分對象放入到老年代。

使用如下jvm參數:

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=40M -XX:+UseConcMarkSweepGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

運行如下代碼:

 1 public static void main(String[] args) {
 2     byte[] b1 = new byte[_1M * 3];
 3     byte[] b2 = new byte[_1M * 8];
 4 
 5     byte[] b3 = new byte[_1M * 30];
 6     b3 = new byte[_1M * 30];
 7     b3 = null;
 8 
 9     byte[] b4 = new byte[_1M * 30]; // 觸發GC
10 }

查看GC日誌:

1 2020-09-26T01:20:03.727+0800: 0.186: [GC (Allocation Failure) 2020-09-26T01:20:03.727+0800: 0.186: [ParNew: 77688K->3799K(92160K), 0.0059624 secs] 77688K->11993K(194560K), 0.0060861 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
2 Heap
3  par new generation   total 92160K, used 36977K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
4   eden space 81920K,  40% used [0x00000000f3800000, 0x00000000f5866800, 0x00000000f8800000)
5   from space 10240K,  37% used [0x00000000f9200000, 0x00000000f95b5e00, 0x00000000f9c00000)
6   to   space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
7  concurrent mark-sweep generation total 102400K, used 8194K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
8  Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
9   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

結合代碼和GC日誌可以看出,YoungGC後,存活的對象無法複製到一個 survivor 區中了,因此有部分對象直接晉陞到老年代了。from survivor 區佔了 37%(3.7M),可以認為是 b1對象(3M)+ 700K左右未知對象;老年代使用了 8194K(8M),就是b2對象(8M)。

需要注意的是,並不是把全部存活的對象晉陞到老年代,而是把部分對象晉陞到老年代,部分複製到 survivor 區中。

7、老年代空間分配擔保原則

如果YougGC時新生代有大量對象存活下來,而 survivor 區放不下了,這時必須轉移到老年代中,但這時發現老年代也放不下這些對象了,那怎麼處理呢?其實JVM有一個老年代空間分配擔保機制來保證對象能夠進入老年代。

在執行每次 YoungGC 之前,JVM會先檢查老年代最大可用連續空間是否大於新生代所有對象的總大小。因為在極端情況下,可能新生代 YoungGC 後,所有對象都存活下來了,而 survivor 區又放不下,那可能所有對象都要進入老年代了。這個時候如果老年代的可用連續空間是大於新生代所有對象的總大小的,那就可以放心進行 YoungGC。但如果老年代的內存大小是小於新生代對象總大小的,那就有可能老年代空間不夠放入新生代所有存活對象,這個時候JVM就會先檢查 -XX:HandlePromotionFailure 參數是否允許擔保失敗,如果允許,就會判斷老年代最大可用連續空間是否大於歷次晉陞到老年代對象的平均大小,如果大於,將嘗試進行一次YoungGC,儘快這次YoungGC是有風險的。如果小於,或者 -XX:HandlePromotionFailure 參數不允許擔保失敗,這時就會進行一次 Full GC。

在允許擔保失敗並嘗試進行YoungGC後,可能會出現三種情況:

  • ① YoungGC後,存活對象小於survivor大小,此時存活對象進入survivor區中
  • ② YoungGC後,存活對象大於survivor大小,但是小於老年大可用空間大小,此時直接進入老年代。
  • ③ YoungGC後,存活對象大於survivor大小,也大於老年大可用空間大小,老年代也放不下這些對象了,此時就會發生「Handle Promotion Failure」,就觸發了 Full GC。如果 Full GC後,老年代還是沒有足夠的空間,此時就會發生OOM內存溢出了。

通過下圖來了解空間分配擔保原則:

分配擔保規則在JDK7之後有些變化,不再判斷 -XX:HandlePromotionFailure 參數。YoungGC發生時,只要老年代的連續空間大於新生代對象總大小,或者大於歷次晉陞的平均大小,就可以進行 YoungGC,否則就進行 FullGC。

下面來結合GC日誌實際觀察下,設置如下jvm參數:老年代100M,eden區80M,survivor區10M,大對象閥值為35M。

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=35M -XX:+UseConcMarkSweepGC -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

運行如下代碼:

 1 public static void main(String[] args) {
 2     byte[] b1 = new byte[_1M * 35];
 3     byte[] b2 = new byte[_1M * 35];
 4 
 5     byte[] b3 = new byte[_1M * 30];
 6     b3 = new byte[_1M * 30];
 7     b3 = null;
 8 
 9     byte[] b4 = new byte[_1M * 30]; // 觸發GC
10 }

查看GC日誌:

 1 2020-09-26T02:53:17.908+0800: 0.210: [GC (Allocation Failure) 2020-09-26T02:53:17.909+0800: 0.210: [ParNew: 66424K->707K(92160K), 0.0008820 secs] 138104K->72387K(194560K), 0.0010026 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 2 2020-09-26T02:53:17.911+0800: 0.213: [GC (CMS Initial Mark) [1 CMS-initial-mark: 71680K(102400K)] 103107K(194560K), 0.0002821 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 3 2020-09-26T02:53:17.912+0800: 0.213: [CMS-concurrent-mark-start]
 4 2020-09-26T02:53:17.912+0800: 0.213: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 5 2020-09-26T02:53:17.912+0800: 0.213: [CMS-concurrent-preclean-start]
 6 2020-09-26T02:53:17.912+0800: 0.213: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 7 2020-09-26T02:53:17.912+0800: 0.213: [CMS-concurrent-abortable-preclean-start]
 8 Heap
 9  par new generation   total 92160K, used 33885K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
10   eden space 81920K,  40% used [0x00000000f3800000, 0x00000000f5866800, 0x00000000f8800000)
11   from space 10240K,   6% used [0x00000000f9200000, 0x00000000f92b0f48, 0x00000000f9c00000)
12   to   space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
13  concurrent mark-sweep generation total 102400K, used 71680K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
14  Metaspace       used 3047K, capacity 4556K, committed 4864K, reserved 1056768K
15   class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

b1、b2 兩個對象超過大對象閥值,將直接進入老年代,因此可以認為歷次進入老年大對象的平均大小為35M。此時老年代還剩餘30M。

代碼第5、6、7行將產生60M垃圾對象,到第9行時 eden 區不足,這時判斷老年代剩餘空間(30M)是否大於新生代所有對象大小(60M),明顯是否;再判斷老年代大小(30M)是否大於歷次晉陞對象的平均大小(35M),也是否。

因此這時就觸發了 Full GC,GC日誌中第1行發生了一次YoungGC,第2~7行是CMS的OldGC。

8、CMS觸發OldGC

CMS回收器有個參數 -XX:CMSInitiatingOccupancyFraction 來控制當老年代內存佔用超過這個比例後,就觸發CMS回收。因為CMS要預留一些空間保證在回收期間,可以讓對象進入老年代。

設置如下jvm參數:當老年代超過80%時,觸發CMS回收,CMS GC線程每個2秒檢查一次是否回收。

-Xms200M -Xmx200M -Xmn100M -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=15M -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSWaitDuration=2000 
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log

運行如下代碼:

 1 public static void main(String[] args) {
 2     byte[] b3 = new byte[_1M * 30];
 3     b3 = new byte[_1M * 30];
 4     b3 = new byte[_1M * 20];
 5 
 6     try {
 7         Thread.sleep(3000);
 8     } catch (InterruptedException e) {
 9         e.printStackTrace();
10     }
11 }

查看GC日誌:

2020-09-26T04:13:52.245+0800: 2.083: [GC (CMS Initial Mark) [1 CMS-initial-mark: 81920K(102400K)] 86904K(194560K), 0.0006366 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-09-26T04:13:52.245+0800: 2.084: [CMS-concurrent-mark-start]
2020-09-26T04:13:52.245+0800: 2.084: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-09-26T04:13:52.245+0800: 2.084: [CMS-concurrent-preclean-start]
2020-09-26T04:13:52.246+0800: 2.084: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2020-09-26T04:13:52.246+0800: 2.084: [CMS-concurrent-abortable-preclean-start]
Heap
 par new generation   total 92160K, used 6622K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,   8% used [0x00000000f3800000, 0x00000000f3e77b80, 0x00000000f8800000)
  from space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
  to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 concurrent mark-sweep generation total 102400K, used 81920K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
 Metaspace       used 3048K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 322K, capacity 392K, committed 512K, reserved 1048576K

可以看到,老年代超過80%後,觸發了一次CMS老年代回收,注意並不是Full GC,只是老年代回收。

還需注意的是,並不是超過80%就立即觸發CMS回收,CMS自己有個間隔時間,通過 -XX:CMSWaitDuration 參數設置,其默認值為2000毫秒,從這裡也可以看出,程序運行2秒後才觸發了CMS的回收。

9、總結

1)內存參數設置

2)垃圾回收觸發時機

參考

本文是學習、參考了如下書籍和課程,再通過自己的總結和實踐總結而來。如果想了解更多深入的細節,建議閱讀原著。

《深入理解Java虛擬機:JVM高級特性與最佳實踐 第三版》

《極客時間:Java性能調優實戰》

從 0 開始帶你成為JVM實戰高手