jstat顯示的full GC次數與CMS週期的關係

語言: CN / TW / HK

使用Oracle/Sun JDK來執行Java程式的時候,大家或許有用過jstat工具來觀察GC的統計資料,例如上一篇日誌裡的

Command prompt程式碼

<code>$ jstat -gcutil `pgrep -u admin java`  
  S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT     
 37.21   0.00  99.81  12.87  76.82   1767  196.843  3085 2998.088 3194.931  
</code><button>複製</button>

以前寫過一帖說明jstat工具顯示的讀數與jvmstat計數器之前的關係: 用Java獲取full GC的次數?(2)

可以知道,FGC列表示的是full GC次數,對應的jvmstat計數器是sun.gc.collector.1.invocations:

Text程式碼

<code>column {  
  header "^FGC^"    /* Full Collections */  
  data sun.gc.collector.1.invocations  
  align right  
  width 5  
  scale raw  
  format "0"  
}  
</code><button>複製</button>

但是這個計數器在CMS GC裡到底是指什麼的次數呢?

在JDK 6的HotSpot VM中,Oracle/Sun有官方支援的GC只有CMS比較特殊( Garbage-First在JDK6裡還沒正式支援,不算在內 ):其它幾種GC的每個週期都是完全stop-the-world的;而CMS的每個併發GC週期則有兩個stop-the-world階段—— initial mark與final re-mark ,其它階段是與應用程式一起併發執行的。

Memory Management in the Java HotSpot™ Virtual Machine 寫道

如果CMS併發GC過程中出現了concurrent mode failure的話那麼接下來就會做一次mark-sweep-compact的full GC,這個是完全stop-the-world的。

正是這個特徵,使得CMS的每個併發GC週期總共會更新full GC計數器兩次,initial mark與final re-mark各一次;如果出現concurrent mode failure,則接下來的full GC自己算一次。

如果說大家關心“GC次數”主要關心的其實是應用暫停次數的話,這麼做倒也合理。但要注意的是在CMS裡“暫停次數”並不等同於“GC次數”,CMS併發GC的一個週期叫“一次GC”但暫停了兩次。

只不過有些人在從其它GC改為用CMS的時候會對“full GC次數”的顯著增加感到不滿,覺得是不是應該想辦法調優來讓“full GC次數”降下來。這裡有幾點:

1、CMS GC的設計初衷就是以降低GC latency為目標。如果一個應用產生垃圾的速度非常高的話,原本清除那些垃圾需要的時間並不會消失,CMS只是把它從一個大暫停分散到了多個階段上,其中部分是暫停的,部分是併發的。所以暫停的次數本來就應該會增加,而每次停頓的時間則應該比較短——這是設計取捨的傾向性導致的。

2、為了更有效的實現併發,CMS GC進行的過程中必須保證堆裡還有足夠剩餘空間來留給應用去分配物件,所以比起ParallelScavenge等別的實現CMS必須要提早一些觸發併發GC的啟動。如果從ParallelScavange遷移到CMS的時候不同事增大GC堆的大小,那麼可以看到同樣的應用在GC堆的佔用率更低的時候就會觸發GC了,所以GC次數增加了。

3、CMS GC中,“full GC次數”的計數器在每個併發GC週期裡是增加2而不是增加1的。這也就是這篇日誌最想說明的點:這個計數器說明了GC造成的應用暫停的次數,但並不代表CMS的併發GC週期的個數。由於full GC的計數器也會在完全stop-the-world的full GC中增加1,所以這個計數器也不準確代表併發GC週期個數的正好兩倍。

4、一個CMS併發GC週期的觸發原因只有一個;其中的兩次暫停都是同一個原因引致的,例如說最初CMS old gen或者perm gen的使用率已經超過了某個閾值之類。

實現細節感興趣的同學們,看程式碼~

CMSCollector裡有_gc_counters用於記錄jvmstat(或者說PerfData)需要的統計資料。這是個CollectorCounters型別的物件,裡面有_invocations成員是用來記錄GC次數的。

C++程式碼

CollectorCounters::CollectorCounters(const char* name, int ordinal) {  
  
  if (UsePerfData) {  
    EXCEPTION_MARK;  
    ResourceMark rm;  
  
    const char* cns = PerfDataManager::name_space("collector", ordinal);  
  
    _name_space = NEW_C_HEAP_ARRAY(char, strlen(cns)+1);  
    strcpy(_name_space, cns);  
  
    // ...  
  
    cname = PerfDataManager::counter_name(_name_space, "invocations");  
    _invocations = PerfDataManager::create_counter(SUN_GC, cname,  
                                                   PerfData::U_Events, CHECK);  
      
    // ...  
  }  
}  

TraceCollectorStats用於輔助記錄GC的次數。它在構造器裡會將傳入的CollectorCounters的invocation_counter()計數器自增1(自增1的具體邏輯在其基類的PerfTraceTimedEvent的構造器裡)。

C++程式碼

class TraceCollectorStats: public PerfTraceTimedEvent {  
  
  protected:  
    CollectorCounters* _c;  
  
  public:  
    inline TraceCollectorStats(CollectorCounters* c) :  
           PerfTraceTimedEvent(c->time_counter(), c->invocation_counter()),  
           _c(c) {  
  
      if (UsePerfData) {  
         _c->last_entry_counter()->set_value(os::elapsed_counter());  
      }  
    }  
  
    inline ~TraceCollectorStats() {  
      if (UsePerfData) _c->last_exit_counter()->set_value(os::elapsed_counter());  
    }  
};  

C++程式碼

class PerfTraceTimedEvent : public PerfTraceTime {  
  
  protected:  
    PerfLongCounter* _eventp;  
  
  public:  
    inline PerfTraceTimedEvent(PerfLongCounter* timerp, PerfLongCounter* eventp): PerfTraceTime(timerp), _eventp(eventp) {  
      if (!UsePerfData) return;  
      _eventp->inc();  
    }  
  
    inline PerfTraceTimedEvent(PerfLongCounter* timerp, PerfLongCounter* eventp, int* recursion_counter): PerfTraceTime(timerp, recursion_counter), _eventp(eventp) {  
      if (!UsePerfData) return;  
      _eventp->inc();  
    }  
};  

計數器增加1就是這裡的_eventp->inc();

HotSpot裡每個stop-the-world行為都用一個VM_Operation包裝起來。與CMS相關的兩個VM_Operation就是VM_CMS_Initial_Mark與VM_CMS_Final_Mark。

C++程式碼

// The VM_CMS_Operation is slightly different from  
// a VM_GC_Operation -- and would not have subclassed easily  
// to VM_GC_Operation without several changes to VM_GC_Operation.  
// To minimize the changes, we have replicated some of the VM_GC_Operation  
// functionality here. We will consolidate that back by doing subclassing  
// as appropriate in Dolphin.  
//  
//  VM_Operation  
//    VM_CMS_Operation  
//    - implements the common portion of work done in support  
//      of CMS' stop-world phases (initial mark and remark).  
//  
//      VM_CMS_Initial_Mark  
//      VM_CMS_Final_Mark  
//  

這兩個VM_Operation的核心部分都呼叫了下面這個函式:

C++程式碼

void CMSCollector::do_CMS_operation(CMS_op_type op) {  
  gclog_or_tty->date_stamp(PrintGC && PrintGCDateStamps);  
  TraceCPUTime tcpu(PrintGCDetails, true, gclog_or_tty);  
  TraceTime t("GC", PrintGC, !PrintGCDetails, gclog_or_tty);  
  TraceCollectorStats tcs(counters());  
  
  switch (op) {  
    case CMS_op_checkpointRootsInitial: {  
      SvcGCMarker sgcm(SvcGCMarker::OTHER);  
      checkpointRootsInitial(true);       // asynch  
      if (PrintGC) {  
        _cmsGen->printOccupancy("initial-mark");  
      }  
      break;  
    }  
    case CMS_op_checkpointRootsFinal: {  
      SvcGCMarker sgcm(SvcGCMarker::OTHER);  
      checkpointRootsFinal(true,    // asynch  
                           false,   // !clear_all_soft_refs  
                           false);  // !init_mark_was_synchronous  
      if (PrintGC) {  
        _cmsGen->printOccupancy("remark");  
      }  
      break;  
    }  
    default:  
      fatal("No such CMS_op");  
  }  
}  

留意到其中的TraceCollectorStats tcs(counters());了麼?這就讓full GC的計數器增加了1。

也就是說CMS GC的兩個暫停階段各自會讓full GC計數器增加1,於是整個CMS併發GC週期裡該計數器就會增加2了。

追加:有人提醒我有這麼一篇文章:

JDK6 Update 23 changes CMS Collection counters

所以順便一提,我這篇blog用的是JDK 6 update 25對應的HotSpot 20來講的。

如果大家關注JMX讀出來CMS collections次數,請留意一下上面連結的文章。