Java-Parallel GC介紹

JVM 垃圾收集器發展歷史

image-20201205221520220

JDK1.8中使用 jmap -heap pid上面會出現 Parallel GC
jmap -heap 18378
Attaching to process ID 18378, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.261-b12

using thread-local object allocation.
Parallel GC with 4 thread(s) ###
JVM垃圾收集器的發展歷史中,咱們並無找到 Parallel GC,那麼它到底表明什麼?

Parallel GC有兩種組合

  • 使用-XX:+UseParallelGC參數來啓用Parallel ScavengePSMarkSweep(Serial Old)收集器組合進行垃圾收集。(圖上能夠找到)
  • 使用-XX:+UserParallelOldGC參數來啓用Parallel scavengeParallel Old收集器組合收集。(圖上能夠找到)

Parallel GC起源

Young GC / Parallel Scavenge

Parallel Scavenge收集器(下稱PS收集器)也是一個多線程收集器,也是使用複製算法,但它的對象分配規則與回收策略都與ParNew收集器有所不一樣,它是以吞吐量最大化(即GC時間佔總運行時間最小)爲目標的收集器實現,它容許較長時間的STW換取總吞吐量最大化。

Full GC / PSMarkSweep(Serial Old)

在Parallel Scavenge收集器架構中自己有PS MarkSweep收集器來進行老年代收集,但因爲PS MarkSweep與Serial Old實現很是接近,所以官方的許多資料都直接以Serial Old代替PS MarkSweep進行講解。

使用-XX:+UseParallelGC參數即是開啓PSScavengePSMarkSweep的組合,便是隻有Young GC是並行的,Full GC仍然是串行,使用標記-整理算法。java

Full GC / PSCompact(ParallelOld GC)

後來開發者開發了基於LISP2算法的並行版的Full GC收集器來收集整個GC堆,名爲 PSCompact。使用 -XX:+UseParallelOldGC參數來即是開啓 PSScavengePSCompact的組合,將 Young GCFull GC都並行化了。

收集器

Parallel Scavenge

新生代並行回收器,內存分佈使用的複製算法。 Parallel Scavenge主要關注的是應用的吞吐量,而其餘收集器關注的主要是儘量的縮短STW(stop the word)的時間。
吞度量=t1/(t1+t2)
t1運行用戶代碼的總時間
t2運行垃圾收集的總時間
好比,虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。
Parallel Scavenge收集器提供了兩個參數來用於精確控制吞吐量,一是控制最大垃圾收集停頓時間的 -XX:MaxGCPauseMillis參數,二是控制吞吐量大小的 -XX:GCTimeRatio參數
  • -XX:MaxGCPauseMillis
參數的值是一個大於0的毫秒數,收集器將盡量的保證回收耗費的時間不超過設定的值,可是,並非越小越好,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的,若是設置的值過小,將會致使頻繁GC,這樣雖然GC停頓時間下來了,可是吞吐量也下來了。好比收集500MB時候,須要每10秒收集一次,每次回收耗時100ms;若是收集300MB的時候,須要每5秒收集一次,每次回收耗時70ms,雖然每次回收耗時更少,可是工做頻次提升,致使吞吐量反而下降了。
  • -XX:GCTimeRatio
參數的值是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,默認值是99,就是容許最大1%(即1/(1+99))的垃圾收集時間。
Parallel Scavenge有個重要的特性,是支持GC自適應的調節策略,使用-XX:UseAdaptiveSizePolicy參數開啓,開啓以後,虛擬機會根據當前系統運行狀況收集監控信息,動態調整新生代的比例、老年大大小等細節參數,以提供最合適的停頓時間或最大的吞吐量。開啓這個參數以後,就不須要再設置新生代大小,Eden與S0/S1的比例等等參數。

Parallel Old

img

Parallel Old GCParallel ScavengeParallel Old收集器組合中,負責Full GC,是一個並行收集器,其在整理年輕代的時候,使用與Parallel Scavenge GC同樣的常規「複製」算法,可是在整理老年代的時候,是使用的基於「標記-整理」算法優化的「Mark–Summary-Compaction」算法。
算法包含三個部分
  • Mark
首先將老年代的內存,劃分爲大小固定的多個連續Region,當標記完存活對象以後,統計每一個Region的存活對象數量。Mark階段採用串行標記全部從GC Roots可直達的對象,而後並行標記全部存活的對象。
  • Summary
某個Region的密度 = 存活對象的內存大小 / Region內存大小。由於每次整理會將存活的對象向Old區的左側移動,而對象存活越久,理論上就越不容易被回收,因此通過屢次整理以後,左側Region中的對象更偏向於穩定、「長壽」,便是左側Region的密度更大。Summary階段,算法採用以空間換時間的優化方式,針對一個密度很大的Region,好比95%的空間是存活對象,只有斷斷續續5%的空間是未使用的,那麼算法認爲這個Region不值得被整理,便是選擇浪費掉這5%的空間,以節省整理操做的時間開銷。在Sumamry階段,首先從左至右計算各個Region的密度,直到找到一個point,這個point左側的Region都不值得整理,右側的Region須要整理。point左側的Region被稱爲dense prefix,這個區域內的對象都不會被移動。Summary階段是一個串行執行的階段。
  • Compaction
Compaction階段利用Summary階段的統計數據,針對須要整理的部分,採用「整理」算法進行並行操做。

GC策略

  • -XX:+ScavengeBeforeFullGC
ScavengeBeforeFullGCParallel GC套裝中(兩種組合都生效)的一個參數,默認是開啓的,做用是在一次Full GC以前,先觸發一次Young GC來清理年輕代,以下降Full GC的STW耗時(Young GC會清理Young GC中非存活的對象,減小Full GC中,標記存活對象的工做量)。

舉個例子,使用System.gc()觸發Full GC,能夠看到日誌以下:算法

2020-03-01T13:38:30.496-0800: [GC (System.gc()) [PSYoungGen: 37274K->1392K(46080K)] 78234K->42360K(97280K), 0.0033397 secs] [Times: user=0.02 sys=0.01, real=0.01 secs] 
2020-03-01T13:38:30.500-0800: [Full GC (System.gc()) [PSYoungGen: 1392K->0K(46080K)] [ParOldGen: 40968K->1225K(51200K)] 42360K->1225K(97280K), [Metaspace: 4876K->4876K(1056768K)], 0.0113851 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]
第一次GC爲一次Young GC,能夠看到是由System.gc()觸發的,而後緊跟着是一次Full GC。

添加-XX:-ScavengeBeforeFullGC參數以後,日誌就變爲只有一條Full GC的日誌:多線程

2020-03-01T14:26:05.562-0800: [Full GC (System.gc()) [PSYoungGen: 37274K->0K(46080K)] [ParOldGen: 40960K->1225K(51200K)] 78234K->1225K(97280K), [Metaspace: 4882K->4882K(1056768K)], 0.0127785 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]

內存分配策略

對於常規收集器來講,當Eden區沒法分配內存時,便會觸發一次Young GC,可是對於Parallel GC有點變化:
  • 當整個新生代剩餘的空間沒法存放某個對象時,Parallel GC中該對象會直接進入老年代;
  • 而若是整個新生代剩餘的空間能夠存放但只是Eden區空間不足,則會嘗試一次Minor GC。

舉個例子:架構

public class TestApp {
    public static void main(String[] args) throws InterruptedException {
        allocM(10);
        allocM(10);
        allocM(10);
        allocM(20);

        Thread.sleep(1000);
    }

    private static byte[] allocM(int n) throws InterruptedException {
        byte[] ret = new byte[1024 * 1024 * n];

        System.out.println(String.format("%s: Alloc %dMB", LocalDateTime.now().toString(), n));
        Thread.sleep(500);

        return ret;
    }
}
JVM參數爲: -Xms100m -Xmx100m -Xmn50m -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+UseParallelOldGC,運行起來,打印日誌以下:
2020-03-01T16:36:13.027: Alloc 10MB
2020-03-01T16:36:13.548: Alloc 10MB
2020-03-01T16:36:14.061: Alloc 10MB
2020-03-01T16:36:14.577: Alloc 20MB
Heap
 PSYoungGen      total 46080K, used 38094K [0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 40960K, 93% used [0x00000007bce00000,0x00000007bf333878,0x00000007bf600000)
  from space 5120K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007c0000000)
  to   space 5120K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfb00000)
 ParOldGen       total 51200K, used 20480K [0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000)
  object space 51200K, 40% used [0x00000007b9c00000,0x00000007bb000010,0x00000007bce00000)
 Metaspace       used 4879K, capacity 5012K, committed 5248K, reserved 1056768K
  class space    used 527K, capacity 564K, committed 640K, reserved 1048576K
能夠看到第4行,分配20M內存時,Eden區已經不足20M空餘內存了,整個年輕代加起來都不夠20M了,可是並無觸發Young GC,而是繼續執行,知道程序結束前,打印堆的狀況,咱們能夠看到20M內存是分配到了老年代中。

修改代碼,將最後一個allocM(20);改爲allocM(5);,從新執行,獲得日誌以下:less

2020-03-01T16:39:56.375: Alloc 10MB
2020-03-01T16:39:56.896: Alloc 10MB
2020-03-01T16:39:57.408: Alloc 10MB

{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 37274K [0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 40960K, 91% used [0x00000007bce00000,0x00000007bf266a08,0x00000007bf600000)
  from space 5120K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007c0000000)
  to   space 5120K, 0% used [0x00000007bf600000,0x00000007bf600000,0x00000007bfb00000)
 ParOldGen       total 51200K, used 0K [0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000)
  object space 51200K, 0% used [0x00000007b9c00000,0x00000007b9c00000,0x00000007bce00000)
 Metaspace       used 4882K, capacity 5012K, committed 5248K, reserved 1056768K
  class space    used 526K, capacity 564K, committed 640K, reserved 1048576K
2020-03-01T16:39:57.910-0800: [GC (Allocation Failure) [PSYoungGen: 37274K->1328K(46080K)] 37274K->1336K(97280K), 0.0033380 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 1328K [0x00000007bce00000, 0x00000007c0000000, 0x00000007c0000000)
  eden space 40960K, 0% used [0x00000007bce00000,0x00000007bce00000,0x00000007bf600000)
  from space 5120K, 25% used [0x00000007bf600000,0x00000007bf74c010,0x00000007bfb00000)
  to   space 5120K, 0% used [0x00000007bfb00000,0x00000007bfb00000,0x00000007c0000000)
 ParOldGen       total 51200K, used 8K [0x00000007b9c00000, 0x00000007bce00000, 0x00000007bce00000)
  object space 51200K, 0% used [0x00000007b9c00000,0x00000007b9c02000,0x00000007bce00000)
 Metaspace       used 4882K, capacity 5012K, committed 5248K, reserved 1056768K
  class space    used 526K, capacity 564K, committed 640K, reserved 1048576K
}

2020-03-01T16:39:57.916: Alloc 5MB
在執行第4行,分配5M內存時,Eden區不足,可是整個年輕代空餘內存是大於5M的,因而觸發了一次Young GC。

悲觀策略

絕大多數收集器,都有這麼一個策略:在執行Young GC以前,若是估計以前晉升老年代的平均大小,比當前老年代的剩餘空間要大的話,則會放棄Young GC,轉而觸發Full GC。

Parallel GC除了上述策略外,還有另一個策略:在執行Young GC以後,若是晉升老年代的平均大小,比當前老年代的剩餘空間要大的話,則會觸發一次Full GC。優化

public class TestApp {
    public static void main(String[] args) throws InterruptedException {
        byte[][] use = new byte[7][];
        use[0] = allocM(10);
        use[1] = allocM(10);
        use[2] = allocM(10);
        use[3] = allocM(10);
        use[4] = allocM(10);
        use[5] = allocM(10);
        use[6] = allocM(10);

        Thread.sleep(1000);
    }

    private static byte[] allocM(int n) throws InterruptedException {
        byte[] ret = new byte[1024 * 1024 * n];

        System.out.println(String.format("%s: Alloc %dMB", LocalDateTime.now().toString(), n));
        Thread.sleep(500);

        return ret;
    }
}
JVM參數爲: -Xms100m -Xmx100m -Xmn50m -XX:SurvivorRatio=8 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -XX:+UseParallelOldGC,運行起來,打印日誌以下(省略掉部分):
2020-03-01T16:02:43.172: Alloc 10MB
2020-03-01T16:02:43.693: Alloc 10MB
2020-03-01T16:02:44.206: Alloc 10MB

{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 37274K [*, *, *)
  eden space 40960K, 91% used [*,*,*)
  from space 5120K, 0% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 0K [*, *, *)
  object space 51200K, 0% used [*,*,*)
2020-03-01T16:02:44.711-0800: [GC (Allocation Failure) [PSYoungGen: 37274K->1392K(46080K)] 37274K->32120K(97280K), 0.0163176 secs] [Times: user=0.09 sys=0.03, real=0.01 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 30728K [*, *, *)
  object space 51200K, 60% used [*,*,*)
}

{Heap before GC invocations=2 (full 1):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 30728K [*, *, *)
  object space 51200K, 60% used [*,*,*)
2020-03-01T16:02:44.728-0800: [Full GC (Ergonomics) [PSYoungGen: 1392K->0K(46080K)] [ParOldGen: 30728K->31945K(51200K)] 32120K->31945K(97280K), [Metaspace: 4881K->4881K(1056768K)], 0.0096352 secs] [Times: user=0.05 sys=0.00, real=0.01 secs] 
Heap after GC invocations=2 (full 1):
 PSYoungGen      total 46080K, used 0K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 0% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 51200K, used 31945K [*, *, *)
  object space 51200K, 62% used [*,*,*)
}

2020-03-01T16:02:44.739: Alloc 10MB
執行完第一次 Young GC以後,因爲年輕代的S區容量不足,因此Eden區中的30M內存會提早晉升到老年代。GC以後,老年代空間被佔用了60%,還剩下40%(20M),而平均晉升內存大小爲30M,因此觸發悲觀策略,致使了一次 Full GC

咱們將JVM中的-Xms100m -Xmx100m換成-Xms120m -Xmx120m,從新執行日誌以下:spa

2020-03-01T16:08:39.372: Alloc 10MB
2020-03-01T16:08:39.895: Alloc 10MB
2020-03-01T16:08:40.405: Alloc 10MB

{Heap before GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 37274K [*, *, *)
  eden space 40960K, 91% used [*,*,*)
  from space 5120K, 0% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 0K [*, *, *)
  object space 71680K, 0% used [*,*,*)
2020-03-01T16:08:40.906-0800: [GC (Allocation Failure) [PSYoungGen: 37274K->1360K(46080K)] 37274K->32088K(117760K), 0.0152322 secs] [Times: user=0.07 sys=0.03, real=0.02 secs] 
Heap after GC invocations=1 (full 0):
 PSYoungGen      total 46080K, used 1360K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 26% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 30728K [*, *, *)
  object space 71680K, 42% used [*,*,*)
}

2020-03-01T16:08:40.923: Alloc 10MB
2020-03-01T16:08:41.429: Alloc 10MB
2020-03-01T16:08:41.934: Alloc 10MB

{Heap before GC invocations=2 (full 0):
 PSYoungGen      total 46080K, used 32854K [*, *, *)
  eden space 40960K, 76% used [*,*,*)
  from space 5120K, 26% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 30728K [*, *, *)
  object space 71680K, 42% used [*,*,*)
2020-03-01T16:08:42.438-0800: [GC (Allocation Failure) [PSYoungGen: 32854K->1392K(46080K)] 63582K->62840K(117760K), 0.0151558 secs] [Times: user=0.07 sys=0.03, real=0.02 secs] 
Heap after GC invocations=2 (full 0):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 61448K [*, *, *)
  object space 71680K, 85% used [*,*,*)
}

{Heap before GC invocations=3 (full 1):
 PSYoungGen      total 46080K, used 1392K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 27% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 61448K [*, *, *)
  object space 71680K, 85% used [*,*,*)
 Metaspace       used 4883K, capacity 5012K, committed 5248K, reserved 1056768K
  class space    used 526K, capacity 564K, committed 640K, reserved 1048576K
2020-03-01T16:08:42.454-0800: [Full GC (Ergonomics) [PSYoungGen: 1392K->0K(46080K)] [ParOldGen: 61448K->62634K(71680K)] 62840K->62634K(117760K), [Metaspace: 4883K->4883K(1056768K)], 0.0139615 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
Heap after GC invocations=3 (full 1):
 PSYoungGen      total 46080K, used 0K [*, *, *)
  eden space 40960K, 0% used [*,*,*)
  from space 5120K, 0% used [*,*,*)
  to   space 5120K, 0% used [*,*,*)
 ParOldGen       total 71680K, used 62634K [*, *, *)
  object space 71680K, 87% used [*,*,*)
}

2020-03-01T16:08:42.469: Alloc 10MB
能夠看到,第一次 Young GC以後,因爲老年代剩餘空間足夠大,並無觸發Full GC,而隨着內存繼續分配,第二次 Young GC以後,仍是觸發了悲觀策略。

JVM默認老年代回收是 PSMarkSweep(Serial-Old) 仍是Parallel Old?

這個改進使得HotSpot VM在選擇使用 ParallelGC(-XX:+UseParallelGC 或者是ergonomics自動選擇)的時候,會默認開啓 -XX:+UseParallelOldGC 。這個變動應該是在JDK7u4開始的JDK7u系列與JDK8系列開始生效。

http://hg.openjdk.java.net/jd....net

--- a/src/share/vm/runtime/arguments.cpp    Mon Jan 30 15:21:57 2012 +0100
+++ b/src/share/vm/runtime/arguments.cpp    Thu Feb 02 16:05:17 2012 -0800
@@ -1400,10 +1400,11 @@
 
 void Arguments::set_parallel_gc_flags() {
   assert(UseParallelGC || UseParallelOldGC, "Error");
-  // If parallel old was requested, automatically enable parallel scavenge.
-  if (UseParallelOldGC && !UseParallelGC && FLAG_IS_DEFAULT(UseParallelGC)) {
-    FLAG_SET_DEFAULT(UseParallelGC, true);
+  // Enable ParallelOld unless it was explicitly disabled (cmd line or rc file).
+  if (FLAG_IS_DEFAULT(UseParallelOldGC)) {
+    FLAG_SET_DEFAULT(UseParallelOldGC, true);
   }
+  FLAG_SET_DEFAULT(UseParallelGC, true);
 
   // If no heap maximum was requested explicitly, use some reasonable fraction
   // of the physical memory, up to a maximum of 1GB.
在這個改變以前,即使選擇了ParallelGC,默認狀況下ParallelOldGC並不會隨即開啓,而是要本身經過 -XX:+UseParallelOldGC 去選定。

在GC日誌裏,若是看到Full GC裏有"ParOldGen"就是選擇了ParallelOldGC。
[Full GC [PSYoungGen: 480K->0K(3584K)] [ParOldGen: 4660K->4909K(12288K)] 5141K->4909K(15872K) [PSPermGen: 11202K->11198K(22528K)], 0.0515530 secs] [Times: user=0.08 sys=0.00, real=0.05 secs]線程

相關文章
相關標籤/搜索