Java虛擬機垃圾回收相關知識點全梳理(下)

1、前言

上一篇文章《Java虛擬機垃圾回收相關知識點全梳理(上)》我整理分享了JVM運行時數據區域的劃分,垃圾斷定算法以及垃圾回收算法,各類算法的適用場景。今天,我整理分享下JVM性能的度量指標,垃圾收集器的分類,最後分享一下JVM的調優建議。html

2、性能度量指標

  • 吞吐量:表示系統減去系統回收時間佔總時間的比率,好比系統運行了100秒,垃圾回收佔用了1秒,那麼吞吐量量就是(100-1)/100=99%。java

  • 垃圾回收消耗:和吞吐量相反,垃圾回收器消耗指垃圾回收器耗時與系統運行總時間的比值。git

  • 停頓時間:指垃圾回收器運行時,系統停頓的時間。github

  • 回收頻率:指垃圾回收器多長時間會運行一次。通常來講,對於固定的應用而言,垃圾回收器的頻率應該是越低越好。一般增大堆空間能夠有效下降垃圾回收發生的頻率,可是可能會增長回收產生的停頓時間。算法

  • 反應時間:當一個內存對象被標記爲垃圾對象後到這個對象被真正回收產生的時間。windows

根據這幾個指標,咱們能夠知道,垃圾回收性能好的表現是:吞吐量高,垃圾回收消耗低,停頓時間少,回收頻率低,反應時間快。可是,並無這麼完美的性能表現,這幾個指標有些是互斥的,好比要下降回收頻率,就要擴大空間,可是就會增長停頓時間;一樣要想反應時間快,就必需要提升回收頻率。因此,這些性能的追求就是一個博弈平衡的過程,咱們能夠根據咱們追求的某一方面來進行調優,好比,對於客戶端應用而言,應該儘量下降其停頓時間,給用戶良好的使用體驗,爲此,能夠犧牲垃圾回收的吞吐量;對服務端程序來講,可能會更加關注吞吐量。bash

3、垃圾回收器

3.1 Serial 收集器

Serial 收集器是全部垃圾收集器中最古老的一種,也是JDK中最基本的垃圾收集器之一。Serial回收器主要有兩個特色:第一:使用單線程進行垃圾回收;第二:獨佔式垃圾回收。網絡

在串行收集器進行垃圾回收時,Java應用程序中的線程都須要暫停,等待垃圾回收完成。這種現象成爲Stop-The-World。它將形成很是糟糕的用戶體驗,在實時性要求較高的應用場景中,這種現象每每是不能被接受的,可是它依然是在Client模式下默認的新生代收集器。在單核CPU環境下,因爲沒有線程間的切換,它甚至比並發收集器的性能都要好。(如下圖片來源於網絡)數據結構

圖片來源於網絡

3.2 ParNew 收集器

ParNew 收集器是Serial 收集器的多線程版本。它的回收策略、算法以及參數和串行回收器同樣。它是許多Server模式下新生代首選的收集器,除了他的多線程回收功能外,還有一點的就是隻有他能與CMS收集器配合工做。開啓ParNew 收集器可使用如下參數:多線程

-XX:+UseParNewGC:新生代使用並行收集器,老年代使用串行回收器。

-XX:+UseConcMarkSweepGC:新生代使用並行回收器,老年代使用CMS。

並行收集器工做時的線程數量可使用 -XX:ParallelGCThreads 參數指定。通常最好與CPU數量至關,避免過多的線程數,影響垃圾收集性能。 在默認狀況下,當CPU數量小於8個時,ParallelGCThreads 的值等於 CPU 數量;當 CPU 數量大於8個時,ParallelGCThreads 的值等於 3+[(5*CPU_Count)/8]

圖片來源於網絡

3.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器是新生代收集器,它是使用複製算法的收集器,同時也是多線程收集器。它和其餘併發收集器不一樣的點是,Parallel Scavenge 收集器 關注吞吐量,其餘的並行收集器關注的是下降停頓時間。 開啓Parallel Scavenge 收集器可使用如下參數:

-XX:+UseParallelGC:新生代使用並行回收收集器,老年代使用串行回收器。

-XX:+UseParallelOldGC:新生代與老年代都使用並行回收收集器。

並行回收收集器提供了兩個重要的參數用於控制系統的吞吐量:

-XX:+MaxGCPauseMills:設置最大垃圾收集停頓時間,它的值是一個大於 0 的整數。收集器在工做時會調整 Java 堆大小或者其餘一些參數,儘量地把停頓時間控制在 MaxGCPauseMills 之內。這裏須要注意的是若是但願減小停頓時間,而把這個值設置得很是小,虛擬機爲了達到預期的停頓時間,JVM 可能會使用一個較小的堆 (一個小堆比一個大堆回收快),而這將致使垃圾回收變得很頻繁,從而增長了垃圾回收總時間,下降了吞吐量。

-XX:+GCTimeRatio:設置吞吐量大小,它的值是一個 0-100 之間的整數。假設 GCTimeRatio 的值爲 n,那麼系統將花費不超過 1/(1+n) 的時間用於垃圾收集。好比 GCTimeRatio 等於 19,則系統用於垃圾收集的時間不超過 1/(1+19)=5%。默認狀況下,它的取值是 99,即不超過 1%的時間用於垃圾收集。

除此以外,Parallel Scavenge 收集器與ParNew 收集器另外一個不一樣之處在於,前者支持一種自適應的 GC 調節策略,使用-XX:+UseAdaptiveSizePolicy 能夠打開自適應 GC 策略。在這種模式下,新生代的大小、eden 和 survivor 的比例、晉升老年代的對象年齡等參數會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。在手工調優比較困難的場合,能夠直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量 (GCTimeRatio) 和停頓時間 (MaxGCPauseMills),讓虛擬機本身完成調優工做。

3.4 Serial Old 收集器

Serial Old 收集器是Serial收集器的老年代版本,從名字咱們就能夠知道,它是一個單線程收集器,使用「標記-整理」算法。該虛擬機的主要使用場景是在Client模式下使用。它是CMS收集器的後備方案,當CMS收集器進行收集的時候,發生了Concurrent Mode Failure時,會觸發使用Serial Old 收集器進行Full GC,此時會帶來長時間的STW,進而影響系統響應,這也是CMS收集器的一個缺點。

圖片來源於網絡

3.5 Parallel Old 收集器

Parallel Old 收集器也是一種多線程併發的收集器。和Parallel Scavenge 收集器同樣,它也是一種關注吞吐量的收集器。Parallel Old 收集器使用標記-壓縮算法。

圖片來源於網絡

3.6 CMS(Concurrent Mark Sweep) 收集器

CMS 收集器是一個以獲取最大回收停頓時間爲目標的收集器,CMS垃圾回收的過程主要分爲5步:初始標記、併發標記、從新標記、併發清除和併發重置。其中初始標記和從新標記是須要進行「Stop The World」,而併發標記、併發清除和併發重置是能夠和用戶線程一塊兒執行的。所以,從總體上來講,CMS 收集不是獨佔式的,它能夠在應用程序運行過程當中進行垃圾回收 。CMS收集器也有三大缺點:

  • 對CPU資源比較敏感,在併發階段,雖然不會致使用戶線程停頓,可是仍是會佔用部分CPU資源,從而致使程序變慢,吞吐量降低。
  • CMS沒法處理浮動垃圾,由於CMS進行垃圾收集是和用戶線程一塊兒運行的,因此在收集的過程當中就會產生垃圾,這部分垃圾就被稱爲浮動垃圾,浮動垃圾只能等待下一次垃圾收集期間進行收集。由於垃圾收集過程與用戶線程一塊兒運行,因此收集過程當中仍是要預留空間給用戶線程使用,若是空間不夠,就會出現「Concurrent Mode Failure」 失敗,接着就會出現備選方案的Serial Old收集器進行Full Gc,會進行長時間的停頓,進而影響性能。
  • CMS收集器是「標記-清除」算法的收集器,因此在垃圾收集事後會帶來大量的內存碎片,CMS提供了一種內存壓縮參數+XX:+UseCMSCompactAtFullCollection(默認是開啓的)開啓後CMS會在進行Full GC 的時候進行內存整理,+XX:CMSFullGCsBeforeCompaction能夠設置執行多少次不壓縮內存後再進行壓縮的Full GC。

來源於網絡

3.7 G1(Garbage-First) 收集器

G1收集器是一款面向服務端的垃圾收集器,在jdk1.7後能夠正式使用,能夠經過命令-XX:+UnlockExperimentalVMOptions –XX:+UseG1G來啓用G1收集器。G1收集器採用的是「標記-整理」算法,它也是一個進行能夠預測停頓時間的垃圾收集器。能夠經過參數設置停頓時間:

-XX:MaxGCPauseMills = 20

-XX:GCPauseIntervalMills = 200。
複製代碼

以上參數指定在200ms內,停頓時間不超過20ms。這兩個參數是G1回收器的目標,G1回收器並不保證能執行它們。 G1收集器的區域分佈以下圖所示:

圖片來源於網絡

在G1中把java堆分紅了多個大小相等的獨立區域(Region),雖然保留了新生代和老年代的概念,可是他們都不是物理隔離的,只是邏輯上還有區分。

G1收集器進行垃圾收集分爲4個階段,初始標記,併發標記,最終標記,篩選回收。初始標記須要停頓用戶線程,可是時間很短;併發標記是從GC Roots對堆中的對象進行可達性分析,這個階段比較耗時,可是能夠與用戶線程併發執行;最終標記是修正在併發標記中產生的變更;篩選回收就是對標記好的垃圾對象進行價值和成本排序,根據用戶設定的指望來進行回收(好比咱們上面設置的200ms停頓時間不超過20ms)。

3.8 ZGC(Z Garbage Collector) 收集器

ZGC 被稱爲「一個可伸縮低延遲的垃圾回收器」,這個垃圾回收器有什麼神奇之處呢?它的主要特色就是能把回收時間控制在10ms之內,並且不受堆大小的影響,因此它能夠支持TB級別的垃圾回收。

ZGC也是和G1收集器同樣,並無進行分代,而是把整個內存分紅了多個region,官方後續會嘗試採用分代的設計,目前徹底由於是不分代這是最簡單的設計。一次完整的 ZGC 回收週期分爲如下幾個階段(Phase):

  • Pause Mark Start:標記根對象;

  • Concurrent Mark:併發標記階段;

  • Concurrent Relocate:併發重定位;

    • 活動對象被移動到了一個新的 Heap Region B-region 中,以前舊對象所在的 Heap Region A-region 便可複用;若是 B-region 中對象之間的引用關係將會在這一階段被更新;
    • 在重定位過程當中,新舊對象的映射關係(同一對象在不一樣 Region 中的映射關係)被記錄在了 Forwarding Tables 中。
  • Pause Mark Start:這個階段實際上已經進入了新的 ZGC Cycle,一樣也是標記根對象;

  • Concurrent Remap:併發重映射。 這個階段除了標記根對象直接引用的對象外,還會根據上個 ZGC Cycle 中生成的 Forwarding Tables 更新跨 Heap Region 的引用;

ZGC仍是有停頓的,在Pause Mark Start 階段進行根對象掃描(Root Scanning)時會出現短暫的暫停。 流程示意圖以下(圖片來源於網絡)

4、一些JVM調優建議

4.1將新對象預留在年輕代

衆所周知,因爲 Full GC 的成本遠遠高於 Minor GC,所以某些狀況下須要儘量將對象分配在年輕代,這在不少狀況下是一個明智的選擇。雖然在大部分狀況下,JVM 會嘗試在 Eden 區分配對象,可是因爲空間緊張等問題,極可能不得不將部分年輕對象提早向年老代壓縮。所以,在 JVM 參數調優時能夠爲應用程序分配一個合理的年輕代空間,以最大限度避免新對象直接進入年老代的狀況發生。這裏其實是爲了不「朝生夕滅」的大對象發生,儘量的把設置合理新生代空間,把「朝生夕滅 」對象留在新生代中。

4.2 將大對象直接分配再老年代

咱們分配對象通常都是分配在年輕代,分配大對象在年輕代,須要年輕代提供足夠的空間,這個時候會致使原有的大量小對象進入老年代,佔用老年代空間。基於以上緣由,能夠將大對象直接分配到年老代,從而保留爲年輕代保留了空間,保證了年輕代原有的目的,這樣也能夠提升 GC 的效率。若是一個大對象同時又是一個短命的對象,假設這種狀況出現很頻繁,那對於 GC 來講會是一場災難。本來應該用於存放永久對象的年老代,被短命的對象塞滿,這也意味着對堆空間進行了洗牌,擾亂了分代內存回收的基本思路。所以,在軟件開發過程當中,應該儘量避免使用「朝生夕滅」這樣短命的大對象。可使用參數-XX:PetenureSizeThreshold 設置大對象直接進入年老代的閾值。當對象的大小超過這個值時,將直接在年老代分配。參數-XX:PetenureSizeThreshold 只對串行收集器和年輕代並行收集器有效,並行回收收集器不識別這個參數。

4.3 設置對象進入老年代的年齡

堆中的每個對象都有本身的年齡。通常狀況下,年輕對象存放在年輕代,老年對象存放在老年代。爲了作到這點,虛擬機爲每一個對象都維護一個年齡。若是對象在 Eden 區,通過一次 GC 後依然存活,則被移動到 Survivor 區中,對象年齡加 1。之後,若是對象每通過一次 GC 依然存活,則年齡再加 1。當對象年齡達到閾值時,就移入老年代,成爲老年對象。那麼設置一個合適的老年代的年齡就有利於提高系統性能,能夠經過-XX:MaxTenuringThreshold 來設置,默認值是 15。雖然-XX:MaxTenuringThreshold 的值多是 15 或者更大,但這不意味着新對象非要達到這個年齡才能進入老年代。若是在Survivor空間中相同年齡全部對象的大小總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代。

4.4 穩定的堆與震盪的堆

通常來講,穩定的堆大小對垃圾回收是有利的。得到一個穩定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 同樣。若是這樣設置,系統在運行時堆大小理論上是恆定的,穩定的堆空間能夠減小 GC 的次數。所以,不少服務端應用都會將最大堆和最小堆設置爲相同的數值。穩定的堆大小雖然能夠減小 GC 次數,但同時也增長了每次 GC 的時間。讓堆大小在一個區間中震盪,在系統不須要使用大內存時,壓縮堆空間,使 GC 應對一個較小的堆,能夠加快單次 GC 的速度。基於這樣的考慮,JVM 還提供了兩個參數用於壓縮和擴展堆空間。

XX:MinHeapFreeRatio: 設置堆的最小空閒比例,默認是40,當堆空間的空閒空間小於這個數值時,jvm會自動擴展空間。

-XX:MaxHeapFreeRatio: 設置堆的最大空閒比例,默認是70,當堆空間的空閒空間大於這個數值時,jvm會自動壓縮空間。

當-Xmx 和-Xms 相等時,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 兩個參數無效。
複製代碼

4.5 嘗試使用大的內存分頁

CPU 是經過尋址來訪問內存的。32 位 CPU 的尋址寬度是 0~0xFFFFFFFF ,計算後獲得的大小是 4G,也就是說可支持的物理內存最大是 4G。但在實踐過程當中,碰到了這樣的問題,程序須要使用 4G 內存,而可用物理內存小於 4G,致使程序不得不下降內存佔用。爲了解決此類問題,現代 CPU 引入了 MMU(Memory Management Unit 內存管理單元)。MMU 的核心思想是利用虛擬地址替代物理地址,即 CPU 尋址時使用虛址,由 MMU 負責將虛址映射爲物理地址。MMU 的引入,解決了對物理內存的限制,對程序來講,就像本身在使用 4G 內存同樣。內存分頁 (Paging) 是在使用 MMU 的基礎上,提出的一種內存管理機制。它將虛擬地址和物理地址按固定大小(4K)分割成頁 (page) 和頁幀 (page frame),並保證頁與頁幀的大小相同。這種機制,從數據結構上,保證了訪問內存的高效,並使 OS 能支持非連續性的內存分配。在程序內存不夠用時,還能夠將不經常使用的物理內存頁轉移到其餘存儲設備上,好比磁盤,在windows下,這部分空間叫作虛擬內存,Linux下叫作SWAP分區。

在 Solaris 系統中,JVM 能夠支持 Large Page Size 的使用。使用大的內存分頁能夠加強 CPU 的內存尋址能力,從而提高系統的性能。

java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k –XX:++UseParallelGC
 –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC –XX:+LargePageSizeInBytes=256m
–XX:+LargePageSizeInBytes:設置大頁的大小。
複製代碼

過大的內存分頁會致使 JVM 在計算 Heap 內部分區(perm, new, old)內存佔用比例時,會出現超出正常值的劃分,最壞狀況下某個區會多佔用一個頁的大小

4.6 根據場景選擇合適的收集器

對於對響應時間不敏感的場景,能夠選擇吞吐量優先的收集器來提高性能,好比Parallel Old 收集器。若是是對響應時間要求高的場景,就須要選擇低停頓的垃圾回收器,好比CMS,G1,ZGC(雖然目前還不是很是成熟)。

5、總結

這篇文章內容比較,主要分享了虛擬機的性能度量指標,垃圾回收器的分類,一些調優建議。最後放一張本文的腦圖進行總結:

6、參考

相關文章
相關標籤/搜索