簡書 占小狼 轉(zhuǎn)載請注明原創(chuàng)出處,謝謝!
大家新年好,愿你們在新的一年順利晉升、工資漲漲漲...
之前無意間碰到一個有趣的CMS GC問題,問題很簡單,現(xiàn)象很粗暴。
代碼
/**
* -Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
* -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75
* -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
*/
public class JVM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
byte[] b1 = new byte[2 * _1MB];
byte[] b2 = new byte[2 * _1MB];
byte[] b3 = new byte[2 * _1MB];
byte[] b4 = new byte[4 * _1MB];
System.in.read();
}
}
現(xiàn)象
程序運行之后,執(zhí)行jstat -gcutils pid 1000命令,結(jié)果如下:

在JVM參數(shù)中已經(jīng)設(shè)置了-XX:+UseCMSInitiatingOccupancyOnly和 -XX:CMSInitiatingOccupancyFraction=75
只有在老年代的使用率達到75%時才會觸發(fā)CMS回收,可目前的現(xiàn)象是老年代使用率才60%,就開始不停的GC、不停的GC、不停的GC,GC日志如下:

看這架勢,應(yīng)該是在不停的發(fā)生CMS GC了。

原因查找
既然一直在觸發(fā)CMS,那問題根本因為在觸發(fā)CMS的條件中,之前以為只要設(shè)置了-XX:+UseCMSInitiatingOccupancyOnly參數(shù),只有在老年代的使用率達到閾值時才會觸發(fā)。
翻了代碼之后才發(fā)現(xiàn),問題沒這么簡單,觸發(fā)CMS的判斷邏輯位于CMSCollector::shouldConcurrentCollect()方法中,實現(xiàn)如下:

在設(shè)置了-XX:+UseCMSInitiatingOccupancyOnly參數(shù)的前提下,有三種情況會觸發(fā):
1、老年代當(dāng)前使用率是否達到閾值CMSInitiatingOccupancyFraction;
2、判斷當(dāng)前新生代的對象是否能夠全部順利的晉升到老年代,如果不能,就提早觸發(fā)一次老年代的收集,這是本案例中不停CMS的根本原因,incremental_collection_will_fail(true)實現(xiàn)如下:

其中get_gen(0)指向當(dāng)前年輕代的堆,因為設(shè)置了-XX:+UseParNewGC,則年輕代的堆實現(xiàn)是ParNewGeneration,該類繼承了DefNewGeneration,方法collection_attempt_is_safe()位于DefNewGeneration類中,實現(xiàn)如下:

前面2個條件先忽略,看最后一個條件,_next_gen指向老年代的堆,其中promotion_attempt_is_safe()實現(xiàn)如下:

傳入的參數(shù)max_promotion_in_bytes,由年輕代的used方法計算得到,eden區(qū)的使用量 + from區(qū)的使用量
size_t DefNewGeneration::used() const {
return eden()->used()
+ from()->used(); // to() is only used during scavenge
}
其中promotion_attempt_is_safe()方法中的變量
1、available是老年代的可用內(nèi)存大小
2、av_promo每次YGC時晉升到老年代對象大小的平均值
當(dāng)老年代的可用內(nèi)存大于av_promo,或者大于max_promotion_in_bytes時,說明下次的YGC是安全的,否則返回fasle,提早進行一次CMS操作,釋放老年代的空間,以容納下次YGC晉升上來的對象。
到這里,本文中的例子不斷的進行CMS GC的疑惑應(yīng)該可以解釋清楚了。
別忘了,還有第三種情況:
if (CMSClassUnloadingEnabled && _permGen->should_concurrent_collect()) {
bool res = update_should_unload_classes();
if (res) {
if (Verbose && PrintGCDetails) {
gclog_or_tty->print_cr("CMS perm gen initiated");
}
return true;
}
}
前提是設(shè)置了-XX:+CMSClassUnloadingEnabled,而且_permGen永久帶的內(nèi)存使用率達到了閾值CMSInitiatingPermOccupancyFraction,默認值是92。
即使?jié)M足上面2個條件,還需要一層判斷update_should_unload_classes()

如果一開始永久代大小沒有設(shè)置、或者設(shè)置的很小,很有可能一開始就執(zhí)行CMS,這讓很多同學(xué)表示懷疑,什么都沒做,就給我來一次CMS的日志。