簡(jiǎn)書(shū) 占小狼
轉(zhuǎn)載請(qǐng)注明原創(chuàng)出處,謝謝!
數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁FGC
關(guān)于數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁GC的問(wèn)題,笨神又寫了一篇文章分析,當(dāng)時(shí)因?yàn)闆](méi)有仔細(xì)看,導(dǎo)致還有一些疑惑,于是把垃圾回收算法的實(shí)現(xiàn)重新看了一遍,不過(guò)每次看都會(huì)有不小的收獲,所以源碼不是讀一遍就可以了,隔三差五的回頭看看,說(shuō)不定有些不懂的地方,在下一次的時(shí)候就豁朗了。
關(guān)于數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁FGC,有興趣的同學(xué)可以按下面的步驟動(dòng)手實(shí)踐一下,Java代碼如下:
// jdk1.7 -Xmx500M -Xms500M -Xmn200M -XX:+UseConcMarkSweepGC
// -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=90
public class CrossReference {
private static int unit = 20 * 1024;
public static void main(String[] args) throws Exception{
Thread.sleep(5000);
System.out.println("allocate start************");
allocate();
Thread.sleep(1000);
System.out.println("allocate end************");
System.in.read();
}
private static void allocate() throws Exception{
int size = 1024 * 1024 * 400; // 400M
int len = size / unit;
List<BigObject> list = new ArrayList<>();
for(int i = 1; i <= len; i++){
BigObject bigObject = new BigObject();
list.add(bigObject);
Thread.sleep(1);
System.out.println(i);
}
}
private static class BigObject{
private byte[] foo;
BigObject(){
foo = new byte[unit];
}
}
}
通過(guò)jstat -gcutil <pid> 1000命令,可以展示堆內(nèi)存的使用情況和GC觸發(fā)情況。
1、JVM參數(shù)中沒(méi)有-XX:+CMSScavengeBeforeRemark,jstat 結(jié)果如下:

圖中每個(gè)分代的數(shù)值代表內(nèi)存使用率,可以看出發(fā)生FGC的時(shí)候,并沒(méi)有觸發(fā)YGC,所以老年代的對(duì)象一直回收不了,因?yàn)槔夏甏M(jìn)行GC時(shí),會(huì)把新生代的對(duì)象作為GC root,在本場(chǎng)景中,新生代的對(duì)象不回收,老年代的對(duì)象也無(wú)法回收,而且老年代的內(nèi)存使用率已經(jīng)超過(guò)設(shè)置的閾值90%,所以會(huì)不斷的進(jìn)行FGC。
2、JVM參數(shù)中添加-XX:+CMSScavengeBeforeRemark,結(jié)果如下:

從上面的數(shù)據(jù)可以發(fā)現(xiàn),發(fā)生FGC時(shí),確實(shí)也觸發(fā)了YGC,但是這次YGC并沒(méi)有回收對(duì)象,導(dǎo)致了又不斷的FGC,通過(guò)查看GC日志,可以發(fā)現(xiàn)也是在不斷的打印GC信息,而且每次打印的數(shù)據(jù)(GC前的內(nèi)存使用量和GC后的內(nèi)存使用量)都是一樣的。
現(xiàn)象描述如下:
allocate() 方法執(zhí)行完成后
1.頻繁CMS GC,但是old區(qū)仍然占用大,基本未回收空間
2.添加-XX:+CMSScavengeBeforeRemark參數(shù),remark之前的一次ygc也未回收空間,old區(qū)情況同1
3.在Jprofiler中手動(dòng)Run Full GC,young區(qū),old區(qū)都被正?;厥?br>
4.若Arraylist初始化容量為它需要add的數(shù)量,則不存在上述現(xiàn)象
問(wèn)題如下:
1.allocate() 方法執(zhí)行完成后,堆中存在young-》old, old-》young的循環(huán)引用嗎
2.添加-XX:+CMSScavengeBeforeRemark參數(shù),為何仍然有問(wèn)題,方法執(zhí)行完后在Jprofiler中查看存活對(duì)象Arraylist已經(jīng)沒(méi)有,但是BigObject,byte[]仍然存在,占用約400M內(nèi)存
3.手動(dòng)Run Full GC為何就能正?;厥眨?/p>
為了解答這個(gè)問(wèn)題,就得從Hotspot的實(shí)現(xiàn)源碼入手,看看GC動(dòng)作到底是什么樣的過(guò)程,我們不妨可以假設(shè)一下,確實(shí)觸發(fā)了YGC動(dòng)作,然后記錄了GC信息,但是因?yàn)槟承┰虿](méi)有真正的執(zhí)行YGC過(guò)程。
YGC實(shí)現(xiàn)過(guò)程
新生代內(nèi)存分配失敗觸發(fā)YGC
如果是因?yàn)樾律鷥?nèi)存分配失敗觸發(fā)YGC時(shí),JVM內(nèi)部對(duì)應(yīng)會(huì)生成一個(gè)VM_GenCollectForAllocation對(duì)象,提交到一個(gè)執(zhí)行隊(duì)列中,最終會(huì)由VM Thread執(zhí)行它的doit()方法,可以看一下doit方法
void VM_GenCollectForAllocation::doit() {
SvcGCMarker sgcm(SvcGCMarker::MINOR);
GenCollectedHeap* gch = GenCollectedHeap::heap();
GCCauseSetter gccs(gch, _gc_cause);
_res = gch->satisfy_failed_allocation(_size, _tlab);
assert(gch->is_in_reserved_or_null(_res), "result not in heap");
if (_res == NULL && GC_locker::is_active_and_needs_gc()) {
set_gc_locked();
}
}
這里需要關(guān)注的是satisfy_failed_allocation()方法,定義在GenCollectedHeap中
HeapWord* GenCollectedHeap::satisfy_failed_allocation(size_t size, bool is_tlab) {
return collector_policy()->satisfy_failed_allocation(size, is_tlab);
}
由垃圾回收策略決定執(zhí)行satisfy_failed_allocation方法,實(shí)現(xiàn)如下
HeapWord* GenCollectorPolicy::satisfy_failed_allocation(size_t size,
bool is_tlab) {
GenCollectedHeap *gch = GenCollectedHeap::heap();
GCCauseSetter x(gch, GCCause::_allocation_failure);
HeapWord* result = NULL;
assert(size != 0, "Precondition violated");
if (GC_locker::is_active_and_needs_gc()) {
// GC locker is active; instead of a collection we will attempt
// to expand the heap, if there's room for expansion.
if (!gch->is_maximal_no_gc()) {
result = expand_heap_and_allocate(size, is_tlab);
}
return result; // could be null if we are out of space
} else if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) {
// Do an incremental collection.
gch->do_collection(false /* full */,
false /* clear_all_soft_refs */,
size /* size */,
is_tlab /* is_tlab */,
number_of_generations() - 1 /* max_level */);
} else {
if (Verbose && PrintGCDetails) {
gclog_or_tty->print(" :: Trying full because partial may fail :: ");
}
// Try a full collection; see delta for bug id 6266275
// for the original code and why this has been simplified
// with from-space allocation criteria modified and
// such allocation moved out of the safepoint path.
gch->do_collection(true /* full */,
false /* clear_all_soft_refs */,
size /* size */,
is_tlab /* is_tlab */,
number_of_generations() - 1 /* max_level */);
}
...
1、如果GC_locker正在起作用,說(shuō)明有線程正在通過(guò)JNI操作臨界內(nèi)存,那么就放棄GC動(dòng)作,因?yàn)镴NI操作完之后會(huì)可能會(huì)觸發(fā)一次GC
2、如果上一次YGC是失敗的,至于為什么上一次是失敗的,可能是因?yàn)槔夏甏鷽](méi)有足夠的空間容納新生代的對(duì)象,那么就不執(zhí)行本次的YGC,直接進(jìn)行FGC
不管是YGC還是FGC,都是通過(guò)執(zhí)行GenCollectedHeap::do_collection()方法實(shí)現(xiàn)的,在該方法中會(huì)記錄執(zhí)行GC的日志,實(shí)現(xiàn)如下:
bool complete = full && (max_level == (n_gens()-1));
const char* gc_cause_prefix = complete ? "Full GC" : "GC";
TraceCPUTime tcpu(PrintGCDetails, true, gclog_or_tty);
GCTraceTime t(GCCauseString(gc_cause_prefix, gc_cause()), PrintGCDetails, false, NULL);
其中GCTraceTime實(shí)現(xiàn)中,會(huì)打印GC日志信息,而且這個(gè)時(shí)刻并未開(kāi)始GC動(dòng)作。
CTraceTime::GCTraceTime(const char* title, bool doit, bool print_cr, GCTimer* timer) :
_title(title), _doit(doit), _print_cr(print_cr), _timer(timer) {
if (_doit || _timer != NULL) {
_start_counter = os::elapsed_counter();
}
if (_timer != NULL) {
assert(SafepointSynchronize::is_at_safepoint(), "Tracing currently only supported at safepoints");
assert(Thread::current()->is_VM_thread(), "Tracing currently only supported from the VM thread");
_timer->register_gc_phase_start(title, _start_counter);
}
if (_doit) {
gclog_or_tty->date_stamp(PrintGCDateStamps);
gclog_or_tty->stamp(PrintGCTimeStamps);
gclog_or_tty->print("[%s", title);
gclog_or_tty->flush();
}
}
執(zhí)行YGC的真正邏輯在parNewGeneration::collect()方法中,在方法中有這么一個(gè)判斷邏輯:
// If the next generation is too full to accommodate worst-case promotion
// from this generation, pass on collection; let the next generation
// do it.
if (!collection_attempt_is_safe()) {
gch->set_incremental_collection_failed(); // slight lie, in that we did not even attempt one
return;
}
如果不滿足collection_attempt_is_safe(),就直接返回,說(shuō)明本次YGC是不安全的,不會(huì)真正執(zhí)行本次的垃圾回收,那什么情況算是不安全的呢?collection_attempt_is_safe()實(shí)現(xiàn)如下:
bool DefNewGeneration::collection_attempt_is_safe() {
if (!to()->is_empty()) {
if (Verbose && PrintGCDetails) {
gclog_or_tty->print(" :: to is not empty :: ");
}
return false;
}
if (_next_gen == NULL) {
GenCollectedHeap* gch = GenCollectedHeap::heap();
_next_gen = gch->next_gen(this);
assert(_next_gen != NULL,
"This must be the youngest gen, and not the only gen");
}
return _next_gen->promotion_attempt_is_safe(used());
}
1、to空間不為空
2、沒(méi)有下一個(gè)內(nèi)存代,即沒(méi)有老年代
3、其中情況1和2幾乎不會(huì)發(fā)生,主要還是看這種情況,主要看老年代是否有足夠的空間來(lái)容納新生代的對(duì)象,老年代的promotion_attempt_is_safe()的實(shí)現(xiàn)如下:
bool ConcurrentMarkSweepGeneration::promotion_attempt_is_safe(size_t max_promotion_in_bytes) const {
size_t available = max_available();
size_t av_promo = (size_t)gc_stats()->avg_promoted()->padded_average();
bool res = (available >= av_promo) || (available >= max_promotion_in_bytes);
if (Verbose && PrintGCDetails) {
gclog_or_tty->print_cr(
"CMS: promo attempt is%s safe: available("SIZE_FORMAT") %s av_promo("SIZE_FORMAT"),"
"max_promo("SIZE_FORMAT")",
res? "":" not", available, res? ">=":"<",
av_promo, max_promotion_in_bytes);
}
return res;
}
如果老年代中的可用空間大于gc_stats統(tǒng)計(jì)的新生代每次平均晉升的對(duì)象大小,或者可以容納目前新生代所有的對(duì)象,表明可以執(zhí)行正常的YGC動(dòng)作,如果都不滿足,就直接放棄本次YGC。
由FGC觸發(fā)的YGC
在JVM參數(shù)中添加-XX:+CMSScavengeBeforeRemark,執(zhí)行FGC之前會(huì)觸發(fā)一次YGC,這個(gè)參數(shù)的好處是如果YGC比較有效果的話是能有效降低remark的時(shí)間長(zhǎng)度,可以簡(jiǎn)單理解為如果大部分新生代的對(duì)象被回收了,那么GC root變少了,從而提高了remark的效率。
因?yàn)镃MS是多線程執(zhí)行的,主要的執(zhí)行入口定義在ConcurrentMarkSweepThread::run()方法,ConcurrentMarkSweepThread相當(dāng)于繼承了Java中的Thread的一個(gè)類,在run方法執(zhí)行實(shí)現(xiàn)具體邏輯,下面是CMS執(zhí)行過(guò)程的簡(jiǎn)要分析。
1、run方法中調(diào)用CMSCollector::collect_in_background方法,在該方法中,會(huì)根據(jù)當(dāng)前CMS的執(zhí)行狀態(tài),初始化對(duì)應(yīng)的 VM_CMS_Operation,本文是分析CMSScavengeBeforeRemark,該變量所用到的邏輯中對(duì)用的狀態(tài)為 VM_CMS_Final_Remark,該狀態(tài)是CMS中再次標(biāo)記階段。
2、初始化 VM_CMS_Final_Remark,并提交到VM Thread的執(zhí)行隊(duì)列中,等待被執(zhí)行,最終由VM Thread執(zhí)行它的doit方法,這方式和執(zhí)行YGC時(shí)類似
3、在VM_CMS_Final_Remark::doit()方法中調(diào)用_collector->do_CMS_operation();,最終調(diào)用CMSCollector::checkpointRootsFinal()方法,其中和CMSScavengeBeforeRemark相關(guān)的代碼實(shí)現(xiàn)如下:
if (CMSScavengeBeforeRemark) {
GenCollectedHeap* gch = GenCollectedHeap::heap();
// Temporarily set flag to false, GCH->do_collection will
// expect it to be false and set to true
FlagSetting fl(gch->_is_gc_active, false);
NOT_PRODUCT(GCTraceTime t("Scavenge-Before-Remark",
PrintGCDetails && Verbose, true, _gc_timer_cm);)
int level = _cmsGen->level() - 1;
if (level >= 0) {
gch->do_collection(true, // full (i.e. force, see below)
false, // !clear_all_soft_refs
0, // size
false, // is_tlab
level // max_level
);
}
}
這里又回到了GenCollectedHeap::do_collection()方法,具體過(guò)程和新生代內(nèi)存分配失敗觸發(fā)YGC的情況一樣。
另外手動(dòng)執(zhí)行Run Full GC和執(zhí)行jmap -histo:live <pid> 命令,可以強(qiáng)制虛擬機(jī)執(zhí)行垃圾回收。
參考資料:
假笨說(shuō)-關(guān)于數(shù)組動(dòng)態(tài)擴(kuò)容導(dǎo)致頻繁GC的問(wèn)題,我還有話說(shuō)