jvm(三)——jvm垃圾回收演算法以及實現

通凡發表於2018-09-04

一、概述

java中,垃圾收集 Garbage Collection 通常被稱為“GC”,它誕生於1960年 MIT 的 Lisp 語言,經過半個多世紀,目前已經十分成熟了。

jvm 中,程式計數器、虛擬機器棧、本地方法棧都是都是執行緒私有的,隨執行緒而生隨執行緒而滅,棧幀(棧中的物件)隨著方法的進入和退出做入棧和出棧操作,實現了自動的記憶體清理,因此,我們的記憶體垃圾回收主要集中於 java 堆和方法區中,在程式執行期間,這部分記憶體的分配和使用都是動態的.

二、物件存活判斷

判斷物件是否存活一般有兩種方式:

引用計數:每個物件有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決物件相互迴圈引用的問題。

物件迴圈引用問題,即物件A引用物件B的,而在物件B中又引用了物件A,那麼對於物件A和物件B來說,其引用計數器都為1,難以判斷其是否存活。

可達性分析(Reachability Analysis):從GC Roots開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的,即為不可達物件。

在Java語言中,GC Roots包括:

虛擬機器棧中引用的物件。

方法區中類靜態屬性實體引用的物件。

方法區中常量引用的物件。

本地方法棧中JNI引用的物件。

總結 :GC root包括棧中引用物件和方法區中引用物件。

三、垃圾收集演算法

1、標記 -清除演算法

標記-清除”(Mark-Sweep)演算法,如它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。之所以說它是最基礎的收集演算法,是因為後續的收集演算法都是基於這種思路並對其缺點進行改進而得到的。

它的主要缺點有兩個:一個是效率問題,標記和清除過程的效率都不高;另外一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致,當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

wpsA73E.tmp

2、複製演算法

“複製”(Copying)的收集演算法,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

這樣使得每次都是對其中的一塊進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為原來的一半,降低了記憶體的利用率,持續複製長生存期的物件則導致效率降低,還有在分配物件較大時,該種演算法也存在效率低下的問題。

wps9D31.tmp

3、標記-整理演算法

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

根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,這種演算法克服了複製演算法的低效問題,同時克服了標記清除演算法的記憶體碎片化的問題;

wps3952.tmp

4、分代收集演算法

GC分代的基本假設:絕大部分物件的生命週期都非常短暫,存活時間短。

“分代收集”(Generational Collection)演算法,是一種劃分的策略,把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”演算法來進行回收。

四、垃圾收集器

​ 收集演算法是jvm記憶體回收過程中具體的、通用的方法,垃圾收集器是jvm記憶體回收過程中具體的執行者,即各種GC演算法的具體實現。

1、Serial收集器

序列收集器是最古老,最穩定以及效率高的收集器,可能會產生較長的停頓,只使用一個執行緒去回收。新生代、老年代使用序列回收;新生代複製演算法老年代標記-壓縮;垃圾收集的過程中會Stop The World(服務暫停)

引數控制:-XX:+UseSerialGC 序列收集器

wpsA77.tmp

2、ParNew收集器

ParNew收集器其實就是Serial收集器的多執行緒版本。新生代並行,老年代序列;新生代複製演算法、老年代標記-壓縮

引數控制:-XX:+UseParNewGC ParNew收集器

-XX:ParallelGCThreads 限制執行緒數量引數

wps6A83.tmp

3、Parallel收集器

Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關注系統的吞吐量。可以通過引數來開啟自適應調節策略,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或最大的吞吐量;也可以通過引數控制GC的時間不大於多少毫秒或者比例;新生代複製演算法、老年代標記-壓縮

引數控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代序列

4、Parallel Old 收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。這個收集器是在JDK 1.6中才開始提供

引數控制:-XX:+UseParallelOldGC使用Parallel收集器+ 老年代並行

5、CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用都集中在網際網路站或B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。

從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基於“標記-清除”演算法實現的,它的運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分為4個步驟,包括:

初始標記(CMS initial mark)
併發標記(CMS concurrent mark)
重新標記(CMS remark)
併發清除(CMS concurrent sweep)

其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正併發標記期間,因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
​ 由於整個過程中耗時最長的併發標記和併發清除過程中,收集器執行緒都可以與使用者執行緒一起工作,所以總體上來說,CMS收集器的記憶體回收過程是與使用者執行緒一起併發地執行。老年代收集器(新生代使用ParNew)

優點:併發收集低停頓

缺點:產生大量空間碎片、併發階段會降低吞吐量

引數控制:

-XX:+UseConcMarkSweepGC 使用CMS收集器

**-XX:+ UseCMSCompactAtFullCollection **Full GC後,進行一次碎片整理;整理過程是獨佔的,會引起停頓時間變長

-XX:+CMSFullGCsBeforeCompaction 設定進行幾次Full GC後,進行一次碎片整理

-XX:ParallelCMSThreads 設定CMS的執行緒數量(一般情況約等於可用CPU數量)

wpsCA6E.tmp

6、G1 收集器

G1是目前技術發展的最前沿成果之一,HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中釋出的CMS收集器。與CMS收集器相比G1收集器有以下特點:

  1. 空間整合,G1收集器採用標記整理演算法,不會產生記憶體空間碎片。分配大物件時不會因為無法找到連續空間而提前觸發下一次GC。
  2. 可預測停頓,這是G1的另一大優勢,降低停頓時間是G1和CMS的共同關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為N毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特徵了。

上面提到的垃圾收集器,收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔閡了,它們都是一部分(可以不連續)Region的集合。

wps3B4C.tmp

G1的新生代收集跟ParNew類似,當新生代佔用達到一定比例的時候,開始出發收集。和CMS類似,G1收集器收集老年代物件會有短暫停頓。

收集步驟

1、標記階段,首先初始標記(Initial-Mark),這個階段是停頓的(Stop the World Event),並且會觸發一次普通Mintor GC。對應GC log:GC pause (young) (inital-mark)

2、Root Region Scanning,程式執行過程中會回收survivor區(存活到老年代),這一過程必須在young GC之前完成。

3、Concurrent Marking,在整個堆中進行併發標記(和應用程式併發執行),此過程可能被young GC中斷。在併發標記階段,若發現區域物件中的所有物件都是垃圾,那個這個區域會被立即回收(圖中打X)。同時,併發標記過程中,會計算每個區域的物件活性(區域中存活物件的比例)。

wps93E7.tmp

4、Remark, 再標記,會有短暫停頓(STW)。再標記階段是用來收集 併發標記階段 產生新的垃圾(併發階段和應用程式一同執行);G1中採用了比CMS更快的初始快照演算法:snapshot-at-the-beginning (SATB)。

5、Copy/Clean up,多執行緒清除失活物件,會有STW。G1將回收區域的存活物件拷貝到新區域,清除Remember Sets,併發清空回收區域並把它返回到空閒區域連結串列中。

wps47EC.tmp

6、複製/清除過程後。回收區域的活性物件已經被集中回收到深藍色和深綠色區域。

wpsEAB1.tmp

常用的收集器組合

新生代GC策略 年老代GC策略 說明
組合1 Serial Serial Old
組合2 Serial CMS+Serial Old
組合3 ParNew CMS
組合4 ParNew Serial Old
組合5 Parallel Scavenge Serial Old
組合6 Parallel Scavenge Parallel Old
組合7 G1GC G1GC

五、ZGC

最近一個新的GC收集器概念比較火,JDK團隊在JDK 11中即將迎來ZGC(The Z Garbage Collector),這是一個處於實驗階段的,可擴充套件的低延遲垃圾回收器,本章將對其實現以及效能進行大致介紹,首先說明以下一個指標。

  • 每次GC STW的時間不超過10ms
  • 能夠處理從幾百M到幾T的JAVA堆
  • 與G1相比,吞吐量下降不超過15%
  • 為未來的GC功能和優化利用有色物件指標(colored oops)和載入屏障(load barriers)奠定基礎
  • 初始支援Linux/x64

描述

ZGC的特點:

  • 併發
  • 基於Region的
  • 標記整理
  • NUMA感知
  • 使用colored oops
  • 使用load barrier
  • 僅root掃描時STW,因此GC暫停時間不會隨堆的大小而增加。

ZGC的核心原則是將load barrier與colored oops結合使用。這使得ZGC能夠在Java應用程式執行緒執行時執行併發操作,例如物件遷移時。
從Java執行緒的角度來看,在Java物件中載入引用欄位的行為受到load barrier的影響。除了物件地址之外,colored oops還包含load barrier使用的資訊,以確定在允許Java執行緒使用指標之前是否需要採取某些操作。
例如,物件可能已遷移,在這種情況下,load barrier將檢測情況並採取適當的操作。

與其他替代技術相比,colored oops提供瞭如下非常有吸引力的特性:

  • 它允許ZGC在物件遷移和整理階段回收和重用記憶體。這有助於降低一般堆開銷。這也意味著不需要為Full GC實現一個單獨的標記整理演算法。
  • 目前在colored oops中僅儲存標記和物件遷移相關資訊。然而,這種方案的通用性使我們能夠儲存任何型別的資訊(只要我們可以將它放入指標中)並讓load barrier根據該資訊採取它想要的任何動作。比如,在異構記憶體環境中,這可以用於跟蹤堆訪問模式,以指導GC物件遷移策略,將很少使用的物件移動到冷儲存。

ZGC可以併發執行下面的任務:

  • 標記
  • 引用處置
  • relocation集選擇
  • 遷移和整理

效能

以下是基於同一基準的GC暫停時間。請注意,確切的數字取決於所使用的確切機器和設定。

ZGC
avg: 1.091ms (+/-0.215ms)
95th percentile: 1.380ms
99th percentile: 1.512ms
99.9th percentile: 1.663ms
99.99th percentile: 1.681ms
max: 1.681ms

G1
avg: 156.806ms (+/-71.126ms)
95th percentile: 316.672ms
99th percentile: 428.095ms
99.9th percentile: 543.846ms
99.99th percentile: 543.846ms
max: 543.846ms

限制

  • 當前版本不支援類解除安裝
  • 當前版本不支援JVMCI
    JVMCI是JDK 9 引入的JVM編譯器介面。這個介面允許用Java編寫的編譯器被JVM用作動態編譯器。JVMCI的API提供了訪問VM結構、安裝編譯程式碼和插入JVM編譯系統的機制。現有支援Java編譯器的專案主要是 Graal 和 Metropolis 。

如何工作的

指標標記

在x64系統上,引用是64位的, ZGC重新定義了引用結構

  +-------------------+-+----+-----------------------------------------------+
  |00000000 00000000 0|0|1111|11 11111111 11111111 11111111 11111111 11111111|
  +-------------------+-+----+-----------------------------------------------+
  |                   | |    |
  |                   | |    * 41-0 Object Offset (42-bits, 4TB address space)
  |                   | |
  |                   | * 45-42 Metadata Bits (4-bits)  0001 = Marked0      (Address view 4-8TB)
  |                   |                                 0010 = Marked1      (Address view 8-12TB)
  |                   |                                 0100 = Remapped     (Address view 16-20TB)
  |                   |                                 1000 = Finalizable  (Address view N/A)
  |                   |
  |                   * 46-46 Unused (1-bit, always zero)
  |
  * 63-47 Fixed (17-bits, always zero)

如上表所示, ZGC使用41-0儲存物件實際地址的前42位, 42位地址為應用程式提供了理論4TB的堆空間; 45-42位為metadata位元位, 對應於如下狀態: finalizable,remapped,marked1和marked0; 46位為保留位,固定為0; 63-47位固定為0.

在引用中新增後設資料, 使得解除引用的代價更加高昂, 因為需要操作掩碼以獲取真實的地址, ZGC採用了一種有意思的技巧, 讀操作時是精確知道metadata值的, 而分配空間時, ZGC對映同一頁到3個不同的地址,而在任一時間點,這3個地址中只有一個正在使用中。

for marked0: (0b0001 << 42) | x
for marked1: (0b0010 << 42) | x
for remapped: (0b0100 << 42) | x

實現程式碼如下:

void ZPhysicalMemoryBacking::map(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    map_view(pmem, ZAddress::good(offset), AlwaysPreTouch);
  } else {
    // Map all views
    map_view(pmem, ZAddress::marked0(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::marked1(offset), AlwaysPreTouch);
    map_view(pmem, ZAddress::remapped(offset), AlwaysPreTouch);
  }
}


void ZPhysicalMemoryBacking::unmap(ZPhysicalMemory pmem, uintptr_t offset) const {
  if (ZUnmapBadViews) {
    // Only map the good view, for debugging only
    unmap_view(pmem, ZAddress::good(offset));
  } else {
    // Unmap all views
    unmap_view(pmem, ZAddress::marked0(offset));
    unmap_view(pmem, ZAddress::marked1(offset));
    unmap_view(pmem, ZAddress::remapped(offset));
  }
}

採用此方法後, ZGC堆空間結構如下:

// Address Space & Pointer Layout
// ------------------------------
//
//  +--------------------------------+ 0x00007FFFFFFFFFFF (127TB)
//  .                                .
//  .                                .
//  .                                .
//  +--------------------------------+ 0x0000140000000000 (20TB)
//  |         Remapped View          |
//  +--------------------------------+ 0x0000100000000000 (16TB)
//  |     (Reserved, but unused)     |
//  +--------------------------------+ 0x00000c0000000000 (12TB)
//  |         Marked1 View           |
//  +--------------------------------+ 0x0000080000000000 (8TB)
//  |         Marked0 View           |
//  +--------------------------------+ 0x0000040000000000 (4TB)
//  .                                .
//  +--------------------------------+ 0x0000000000000000

如此帶來一個副作用, ZGC無法相容指標壓縮.

分頁

在G1中,堆記憶體通常被分為幾千個大小相同region。同樣的,在ZGC中堆記憶體也被分成大量的區域,它們被稱為page,不同的是,ZGC中page的大小是不同的。
ZGC有3種不同的頁面型別:小型(2MB大小),中型(32MB大小)和大型(2MB的倍數)。
在小頁面中分配小物件(最大256KB大小),在中間頁面中分配中型物件(最多4MB)。大頁面中分配大於4MB的物件。大頁面只能儲存一個物件,與小頁面或中間頁面相對應。
有些令人困惑的大頁面實際上可能小於中等頁面(例如,對於大小為6MB的大物件)。
這種分配方式讓人想起作業系統的記憶體分配方式,有點相似

標記整理

void ZDriver::run_gc_cycle(GCCause::Cause cause) {
  ZDriverCycleScope scope(cause);
  // Phase 1: Pause Mark Start
  {
    ZMarkStartClosure cl;
    vm_operation(&cl);
  }
  // Phase 2: Concurrent Mark
  {
    ZStatTimer timer(ZPhaseConcurrentMark);
    ZHeap::heap()->mark();
  }
  // Phase 3: Pause Mark End
  {
    ZMarkEndClosure cl;
    while (!vm_operation(&cl)) {
      // Phase 3.5: Concurrent Mark Continue
      ZStatTimer timer(ZPhaseConcurrentMarkContinue);
      ZHeap::heap()->mark();
    }
  }
  // Phase 4: Concurrent Reference Processing
  {
    ZStatTimer timer(ZPhaseConcurrentReferencesProcessing);
    ZHeap::heap()->process_and_enqueue_references();
  }
  // Phase 5: Concurrent Reset Relocation Set
  {
    ZStatTimer timer(ZPhaseConcurrentResetRelocationSet);
    ZHeap::heap()->reset_relocation_set();
  }
  // Phase 6: Concurrent Destroy Detached Pages
  {
    ZStatTimer timer(ZPhaseConcurrentDestroyDetachedPages);
    ZHeap::heap()->destroy_detached_pages();
  }
  // Phase 7: Concurrent Select Relocation Set
  {
    ZStatTimer timer(ZPhaseConcurrentSelectRelocationSet);
    ZHeap::heap()->select_relocation_set();
  }
  // Phase 8: Prepare Relocation Set
  {
    ZStatTimer timer(ZPhaseConcurrentPrepareRelocationSet);
    ZHeap::heap()->prepare_relocation_set();
  }
  // Phase 9: Pause Relocate Start
  {
    ZRelocateStartClosure cl;
    vm_operation(&cl);
  }
  // Phase 10: Concurrent Relocate
  {
    ZStatTimer timer(ZPhaseConcurrentRelocated);
    ZHeap::heap()->relocate();
  }
}

ZGC包含10個階段,但是主要是兩個階段標記和relocating。
GC迴圈從標記階段開始,遞迴標記所有可達物件,標記階段結束時,ZGC可以知道哪些物件仍然存在,哪些是垃圾。ZGC將結果儲存在每一頁的點陣圖(稱為live map)中。

在標記階段,應用執行緒中的load barrier將未標記的引用壓入執行緒本地的標記緩衝區。一旦緩衝區滿,GC執行緒會拿到緩衝區的所有權,並且遞迴遍歷此緩衝區所有可達物件。注意:應用執行緒負責壓入緩衝區,GC執行緒負責遞迴遍歷。

標記階段後,ZGC需要遷移relocate集中的所有物件。relocate集是一組頁面集合,包含了根據某些標準(例如那些包含最多垃圾物件的頁面)確定的需要遷移的頁面。物件由GC執行緒或者應用執行緒遷移(通過load barrier)。ZGC為每個relocate集中的頁面分配了轉發表。轉發表是一個雜湊對映,它儲存一個物件已被遷移到的地址(如果該物件已經被遷移)。

GC執行緒遍歷relocate集的活動物件,並遷移尚未遷移的所有物件。有時候會發生應用執行緒和GC執行緒同時試圖遷移同一個物件,在這種情況下,ZGC使用CAS操作來確定勝利者。

一旦GC執行緒完成了relocate集的處理,遷移階段就完成了。雖然這時所有物件都已遷移,但是舊地引用址仍然有可能被使用,仍然需要通過轉發表重新對映(remapping)。然後通過load barrier或者等到下一個標記迴圈修復這些引用。

這也解釋了為什麼物件引用中有兩個標記位(marked0和marked1)。標記階段交替使用在marked0和marked1位。

load barrier

它的比較容易和CPU的記憶體屏障(memory barrier)弄混淆,但是它們是完全不同的東西。

從堆中讀取引用時,ZGC需要一個所謂的load barrier(也稱為read-barrier)。每次Java程式訪問物件欄位時,ZGC都會執行load barrier的程式碼邏輯,例如obj.field。訪問原始型別的欄位不需要屏障,例如obj.anInt或obj.anDouble。ZGC不使用儲存/寫入障礙obj.field = someValue。

如標記整理章節所說,根據GC當前所處的階段,如果尚未標記或遷移引用,則屏障會標記物件或遷移它。

思考

STW為什麼這麼短

僅root掃描時STW,其他標記、清理、遷移階段,均通過colored oops和load-barrier配合使用,併發執行。

參考資料

JEP 333: ZGC: A Scalable Low-Latency Garbage Collector (Experimental)
http://openjdk.java.net/jeps/333
http://hg.openjdk.java.net/jdk/jdk/rev/767cdb97f103
http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hotspot/os_cpu/linux_x86/zGlobals_linux_x86.hpp#l59
http://hg.openjdk.java.net/zgc/zgc/file/59c07aef65ac/src/hotspot/share/gc/z/zPage.hpp#l34

https://blog.csdn.net/lirenzuo/article/details/81182686

附加內容

Java中Stop-The-World機制簡稱STW,是在執行垃圾收集演算法時,Java應用程式的其他所有執行緒都被掛起(除了垃圾收集幫助器之外)。Java中一種全域性暫停現象,全域性停頓,所有Java程式碼停止,native程式碼可以執行,但不能與JVM互動;這些現象多半是由於gc引起。
GC時的Stop the World(STW)是大家最大的敵人。但可能很多人還不清楚,除了GC,JVM下還會發生停頓現象。
JVM裡有一條特殊的執行緒--VM Threads,專門用來執行一些特殊的VM Operation,比如分派GC,thread dump等,這些任務,都需要整個Heap,以及所有執行緒的狀態是靜止的,一致的才能進行。所以JVM引入了安全點(Safe Point)的概念,想辦法在需要進行VM Operation時,通知所有的執行緒進入一個靜止的安全點。
除了GC,其他觸發安全點的VM Operation包括:
1. JIT相關,比如Code deoptimization, Flushing code cache ;
2. Class redefinition (e.g. javaagent,AOP程式碼植入的產生的instrumentation) ;
3. Biased lock revocation 取消偏向鎖 ;
4. Various debug operation (e.g. thread dump or deadlock check);
監控安全點看看JVM到底發生了什麼?
最簡單的做法,在JVM啟動引數的GC引數裡,多加一句:
-XX:+PrintGCApplicationStoppedTime
它就會把全部的JVM停頓時間(不只是GC),列印在GC日誌裡。
2016-08-22T00:19:49.559+0800: 219.140: Total time for which application threads were stopped: 0.0053630 seconds

這是個很有用的必配引數,可以打出幾乎一切的停頓……
但是,在JDK1.7.40以前的版本,它居然沒有列印時間戳,所以只能知道JVM停了多久,但不知道什麼時候停的。此時一個土辦法就是加多一句“ -XX:+PrintGCApplicationConcurrentTime”,列印JVM在兩次停頓之間的正常執行時間(同樣沒有時間戳),但好歹能配合有時間戳的GC日誌,反推出Stop發生的時間了。
2016-08-22T00:19:50.183+0800: 219.764: Application time: 5.6240430 seconds

如何列印出事哪種原因導致的停頓呢?
再多加兩個引數:-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1

此時,在stdout中會打出類似的內容

vmop [threads: total initially_running wait_to_block]1913.425: GenCollectForAllocation [ 55 2 0 ] [time: spin block sync cleanup vmop] page_trap_count[ 0 0 0 0 6 ] 0

此日誌分兩段,第一段是時間戳,VM Operation的型別,以及執行緒概況
total: 安全點裡的匯流排程數
initially_running: 安全點時開始時正在執行狀態的執行緒數
wait_to_block: 在VM Operation開始前需要等待其暫停的執行緒數
第二行是到達安全點時的各個階段以及執行操作所花的時間,其中最重要的是vmop
spin: 等待執行緒響應
safepoint號召的時間
block: 暫停所有執行緒所用的時間
sync: 等於 spin+block,這是從開始到進入安全點所耗的時間,可用於判斷進入安全點耗時
cleanup: 清理所用時間
vmop: 真正執行VM Operation的時間
可見,那些很多但又很短的安全點,全都是RevokeBias,詳見 偏向鎖實現原理, 高併發的應用一般會乾脆在啟動引數里加一句”-XX:-UseBiasedLocking”取消掉它。另外還看到有些型別是no vm operation, 文件上說是保證每秒都有一次進入安全點(如果這秒已經GC過就不用了),給一些需要在安全點裡進行,又非緊急的操作使用,比如一些取樣型的Profiler工具,可用-DGuaranteedSafepointInterval來調整,不過實際看它並不是每秒都會發生,時間不定。
在實戰中,我們利用安全點日誌,發現過有程式定時呼叫Thread Dump等等情況。不過因為安全點日誌預設輸出到stdout,因為效能及stdout日誌的整潔性等原因,我們平時預設沒有開啟它。只有在需要時才開啟。
再再增加下面三個引數,可以知道更多VM裡發生的事情。可惜JVM不會因為設了這三個引數,就把安全點日誌轉移到vm.log裡面來,而是白白列印了兩次。
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm.log

總結

本文關於快速理解Java垃圾回收和jvm中的stw的介紹就到這裡,希望對大家有所幫助,感興趣的朋友可以參閱:淺談Java回收物件的標記和物件的二次標記過程 、Java虛擬機器裝載和初始化一個class類程式碼解析 、Java中map遍歷方式的選擇問題詳解等,有什麼問題可以隨時留言,小編會及時回覆大家的。

相關文章