您的位置:首页 > 编程语言 > Java开发

jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

2017-09-09 14:46 633 查看
从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨

个人源码地址:https://github.com/FlashLightNing/openjdk-notes

还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk

jvm源码阅读笔记[1]:如何触发一次CMS回收

jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解

jvm源码阅读笔记[3]:从内存分配到触发GC的细节

jvm源码阅读笔记[4]:从GC说到vm operation

jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?

第3篇文章中,我们总结到,当分配内存失败时,会通过VM触发一次由分配失败触发的一次GC,也就是我们经常能在GC日志里面看到的“allocation failure”

VM_GenCollectForAllocation op(size, is_tlab, gc_count_before);//VM操作
VMThread::execute(&op);


同时,在我们第4篇中也简单介绍了VMThread和VMOperation的原理和作用,写到每个VM操作的具体实现逻辑都是在它的doit()方法上。那么今天我们就来看看VM_GenCollectForAllocation的具体GC的过程和步骤。来看看vmGCOperations.cpp:

void VM_GenCollectForAllocation::doit() {
SvcGCMarker sgcm(SvcGCMarker::MINOR);

GenCollectedHeap* gch = GenCollectedHeap::heap();
GCCauseSetter gccs(gch, _gc_cause);
//通知内存堆管理器处理一次内存分配失败
_res = gch->satisfy_failed_allocation(_size, _tlab);//res=分配的结果
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();
}
}


方法很短,精华内容应该就是在gch->satisfy_failed_allocation(_size, _tlab)一行了。

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()) {//表示有jni在操作内存,此时不能进行GC避免改变对象在内存的位置。详见第3篇文章。
if (!gch->is_maximal_no_gc()) {//尽量不gc
result = expand_heap_and_allocate(size, is_tlab);//扩堆
}
return result;
} else if (!gch->incremental_collection_will_fail(false /* don't consult_young */)) {

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 :: ");
}
// 做一次full gc
gch->do_collection(true             /* full */,
false            /* clear_all_soft_refs */,
size             /* size */,
is_tlab          /* is_tlab */,
number_of_generations() - 1 /* max_level */);
}

//GC完再尝试分配
result = gch->attempt_allocation(size, is_tlab, false /*first_only*/);

if (result != NULL) {
assert(gch->is_in_reserved(result), "result not in heap");
return result;
}
//如果GC完还分配失败,看看能否进行扩容和分配
result = expand_heap_and_allocate(size, is_tlab);
if (result != NULL) {
return result;
}

{
UIntFlagSetting flag_change(MarkSweepAlwaysCompactCount, 1); // Make sure the heap is fully compacted

/*
最后再进行一次full gc,同时清除软引用
*/
gch->do_collection(true             /* full */,
true             /* clear_all_soft_refs */,
size             /* size */,
is_tlab          /* is_tlab */,
number_of_generations() - 1 /* max_level */);
}
/*
要是再分配不了,只能报OOM了。。。
*/
result = gch->attempt_allocation(size, is_tlab, false /* first_only */);
if (result != NULL) {
assert(gch->is_in_reserved(result), "result not in heap");
return result;
}

assert(!should_clear_all_soft_refs(),
"Flag should have been handled and cleared prior to this point");

return NULL;
}


总结起来,大致内存分配失败触发的GC分为以下几个过程

.判断此时是否有JNI在操作,若有,则只能通过扩堆然后分配解决,不能进行GC(原因详见第3篇文章)。

若无jni操作,则判断!gch->incremental_collection_will_fail条件。若true,则只是进行一次young gc。若false,则会触发一次fullgc(该次不会清除软引用)。

尝试进行一次内存分配。若成功则返回,若失败,则试着看看能否扩容并分配

若仍失败,最后进行一次full gc,此时会清除软引用。GC完后进行分配。

若还是失败…只能OOM了。

再来解释一下gch->incremental_collection_will_fail是干嘛的?

bool incremental_collection_will_fail(bool consult_young) {
assert(heap()->collector_policy()->is_two_generation_policy(),
"the following definition may not be suitable for an n(>2)-generation system");
return incremental_collection_failed() ||
(consult_young && !get_gen(0)->collection_attempt_is_safe());
}


consult_young=true的时候,表示调用该方法时,判断此时晋升是否的安全的。若=false,表示只取上次young gc时设置的参数,此次不再进行额外的判断

简单来讲,就是

在young gc时可能存在晋升失败的风险。老年代最大的连续可用的空间>之前的平均晋升大小,或者>年轻代使用的空间大小,则被认为是安全的。反之则是不安全

若在执行第2步的ygc时(也就是之前的ygc没有设置晋升失败的标记),young gen在进行gc时判断了晋升情况,认为不安全,就会快速返回,从而让jvm执行第4步中的full gc。这样就达到了在晋升可能失败的情况下,由fullgc 来接替young gc的目的。当进行fullgc或者一次background式的GC后(详见第1篇文章),incremental_collection_will_fail标志就会清除。

打个比方:当系统刚开始运行后,分配了许多对象后,内存空间不够了,第一次触发一次GC。假设此时没有jni,则判断上次ygc是否设置了晋升不安全的标记。因为是此时是第一次触发,所以标记位是默认值(安全的)。那么jvm就试着开始一次ygc了。在ygc的过程中,young gen发现此时老年代放不下年轻代晋升的对象,就直接return,不进行下一步的操作了。方法return之后,走到第3步分配。分配失败然后看看能否扩堆。发现都不行,再执行第4步进行一次full gc(同时清除了晋升不安全的标记),完了之后再分配。 这样就走完了一次完整的分配失败触发GC的过程。

以下是对此部分流程的图解。图片比较长,分成2部分….





下一篇文章将写一下图中的进行young gc和进行full gc的具体流程。

从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨

个人源码地址:https://github.com/FlashLightNing/openjdk-notes

还有一个openjdk6,7,8,9的地址:https://github.com/dmlloyd/openjdk

jvm源码阅读笔记[1]:如何触发一次CMS回收

jvm源码阅读笔记[2]:你不知道的晋升阈值TenuringThreshold详解

jvm源码阅读笔记[3]:从内存分配到触发GC的细节

jvm源码阅读笔记[4]:从GC说到vm operation

jvm源码阅读笔记[5]:内存分配失败触发的GC究竟对内存做了什么?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息