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

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

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

个人源码地址: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究竟对内存做了什么?

对于配置使用CMS回收器的应用,用jstack pid | grep GC 可以发现,有一个名为Concurrent Mark-Sweep GC Thread的线程。



这个线程的作用呢,就是每隔2秒钟,就检查一下老年代是否需要回收,如果需要,则就会进行一次CMS回收。为什么这么说呢?来看看这个线程对应的源码:源码地址

ConcurrentMarkSweepThread::ConcurrentMarkSweepThread(CMSCollector* collector)
: ConcurrentGCThread() {
assert(UseConcMarkSweepGC,  "UseConcMarkSweepGC should be set");//表示在vm启动参数中必须加上UseConcMarkSweepGC
assert(_cmst == NULL, "CMS thread already created");
_cmst = this;
assert(_collector == NULL, "Collector already set");
_collector = collector;
//前面的这些assert主要是一些参数判断,保证这些参数符合条件。
set_name("Concurrent Mark-Sweep GC Thread");//可以看到在这设置了线程名称

if (os::create_thread(this, os::cgc_thread)) {
int native_prio;
if (UseCriticalCMSThreadPriority) {//UseCriticalCMSThreadPriority默认false
native_prio = os::java_to_os_priority[CriticalPriority];
} else {
native_prio = os::java_to_os_priority[NearMaxPriority];
}
os::set_native_priority(this, native_prio);

if (!DisableStartThread) {
os::start_thread(this);
}
}
_sltMonitor = SLT_lock;
assert(!CMSIncrementalMode || icms_is_enabled(), "Error");
}


再来看看比较关键的线程的run()方法:

//线程的run方法
void ConcurrentMarkSweepThread::run() {
assert(this == cmst(), "just checking");

this->record_stack_base_and_size();
this->initialize_thread_local_storage();
this->set_active_handles(JNIHandleBlock::allocate_block());
// 检查是否为当前线程
assert(this == Thread::current(), "just checking");
if (BindCMSThreadToCPU && !os::bind_to_processor(CPUForCMSThread)) {
warning("Couldn't bind CMS thread to processor " UINTX_FORMAT, CPUForCMSThread);
}
{
CMSLoopCountWarn loopX("CMS::run", "waiting for "
"Universe::is_fully_initialized()", 2);
MutexLockerEx x(CGC_lock, true);
set_CMS_flag(CMS_cms_wants_token);
//阻塞到堆初始化完成且所有初始化工作都做完了
while (!is_init_completed() && !Universe::is_fully_initialized() &&
!_should_terminate) {
CGC_lock->wait(true, 200);
loopX.tick();
}
CMSLoopCountWarn loopY("CMS::run", "waiting for SLT installation", 2);
while (_slt == NULL && !_should_terminate) {
CGC_lock->wait(true, 200);
loopY.tick();
}
clear_CMS_flag(CMS_cms_wants_token);
}
//_should_terminate变量只有在stop方法中才会为true,所以只要没有调用stop方法,这个就是while死循环,一直会检测
while (!_should_terminate) {
sleepBeforeNextCycle();//该方法内部也是一个while循环,只有在_should_terminate=true或者是需要进行一次GC时才会结束该方法然后往下运行。具体看后面的分析
if (_should_terminate) break;

//判断此次GC的类型
GCCause::Cause cause = _collector->_full_gc_requested ?
_collector->_full_gc_cause : GCCause::_cms_concurrent_mark;
_collector->collect_in_background(false, cause);//进行background回收
}
assert(_should_terminate, "just checking");
verify_ok_to_terminate();
{
MutexLockerEx mu(Terminator_lock,
Mutex::_no_safepoint_check_flag);
assert(_cmst == this, "Weird!");
_cmst = NULL;
Terminator_lock->notify();
}

// Thread destructor usually does this..
ThreadLocalStorage::set_thread(NULL);
}


其中,关键的是调用的sleepBeforeNextCycle()方法:

void ConcurrentMarkSweepThread::sleepBeforeNextCycle() {
while (!_should_terminate) {
if (CMSIncrementalMode) {//CMS增量回收模式,默认false
icms_wait();
if(CMSWaitDuration >= 0) {
wait_on_cms_lock_for_scavenge(CMSWaitDuration);
}
return;
} else {
//CMSWaitDuration 默认是2000
if(CMSWaitDuration >= 0) {
wait_on_cms_lock_for_scavenge(CMSWaitDuration);//该方法类似于j.u.c里面while循环+超时等待,会阻塞线程CMSWaitDuration毫秒时间
} else {
wait_on_cms_lock(CMSCheckInterval);
}
}
//根据一定条件判断是否需要进行一次CMSGC
if (_collector->shouldConcurrentCollect()) {
return;
}
}
}


根据CMSWaitDuration参数,我们可以知道,该cms线程会一直在sleepBeforeNextCycle()方法内循环,然后阻塞完2000毫秒后,判断是否需要进行一次GC。如果不需要,继续走上面的流程。如果需要,就结束sleepBeforeNextCycle()方法,然后在run方法中,进行一次_collector->collect_in_background()调用,从而回收老年代。

需要注意的是,这里进行的是background式的CMS GC。还有一种是foreground式的CMS GC.具体它们的区别,可以参考这篇文章:http://lovestblog.cn/blog/2015/05/07/system-gc/

所以总结起来就是:当堆初始化完成后,就会创建一个cms的 gc线程。这个线程的作用,就是根据配置的CMSWaitDuration时间(默认2000ms),轮询判断老年代是否需要进行GC(根据一定的条件,比如占用率达到阈值)。如果需要GC,则进行一次background式的CMS GC。

那么,再来看看满足哪些条件的时候jvm会认为需要进行一次CMS GC呢?关键看看concurrentMarkSweepGeneration.cpp中的shouldConcurrentCollect()方法。(concurrentMarkSweepGeneration.cpp地址

//方法开始:判断是否要进行并发回收
bool CMSCollector::shouldConcurrentCollect() {
if (_full_gc_requested) {//发起了一次full gc时
if (Verbose && PrintGCDetails) {
gclog_or_tty->print_cr("CMSCollector: collect because of explicit "
" gc request (or gc_locker)");
}
return true;
}

NOT_PRODUCT(
if (RotateCMSCollectionTypes &&
(_cmsGen->debug_collection_type() !=
ConcurrentMarkSweepGeneration::Concurrent_collection_type)) {
assert(_cmsGen->debug_collection_type() !=
ConcurrentMarkSweepGeneration::Unknown_collection_type,
"Bad cms collection type");
return false;
}
)

FreelistLocker x(this);
// Print out lots of information which affects the initiation of a collection.
if (PrintCMSInitiationStatistics && stats().valid()) {
gclog_or_tty->print("CMSCollector shouldConcurrentCollect: ");
gclog_or_tty->stamp();
gclog_or_tty->cr();
stats().print_on(gclog_or_tty);
gclog_or_tty->print_cr("time_until_cms_gen_full %3.7f",
stats().time_until_cms_gen_full());
gclog_or_tty->print_cr("free="SIZE_FORMAT, _cmsGen->free());
gclog_or_tty->print_cr("contiguous_available="SIZE_FORMAT,
_cmsGen->contiguous_available());
gclog_or_tty->print_cr("promotion_rate=%g", stats().promotion_rate());
gclog_or_tty->print_cr("cms_allocation_rate=%g", stats().cms_allocation_rate());
gclog_or_tty->print_cr("occupancy=%3.7f", _cmsGen->occupancy());
gclog_or_tty->print_cr("initiatingOccupancy=%3.7f", _cmsGen->initiating_occupancy());
gclog_or_tty->print_cr("cms_time_since_begin=%3.7f", stats().cms_time_since_begin());
gclog_or_tty->print_cr("cms_time_since_end=%3.7f", stats().cms_time_since_end());
gclog_or_tty->print_cr("metadata initialized %d",
MetaspaceGC::should_concurrent_collect());
}

/*
UseCMSInitiatingOccupancyOnly默认false
如果并不是只有在占用率达到要求的时候才回收,则会动态进行计算
如果预计的完成CMS回收的所需要的时间小于预计的老年代填满的时间,则进行回收。
*/
if (!UseCMSInitiatingOccupancyOnly) {
if (stats().valid()) {
if (stats().time_until_cms_start() == 0.0) {
return true;
}
} else {
if (_cmsGen->occupancy() >= _bootstrap_occupancy) {//_bootstrap_occupancy默认50%
if (Verbose && PrintGCDetails) {
gclog_or_tty->print_cr(
" CMSCollector: collect for bootstrapping statistics:"
" occupancy = %f, boot occupancy = %f", _cmsGen->occupancy(),
_bootstrap_occupancy);
}
return true;
}
}
}

if (_cmsGen->should_concurrent_collect()) {
if (Verbose && PrintGCDetails) {
gclog_or_tty->print_cr("CMS old gen initiated");
}
return true;
}

/*
如果认为一个增量式的回收可能失败,我们就会开始一次回收。这在实践中不太可能生效,因为这时候已经太迟了。
*/
GenCollectedHeap* gch = GenCollectedHeap::heap();
assert(gch->collector_policy()->is_two_generation_policy(),
"You may want to check the correctness of the following");
if (gch->incremental_collection_will_fail(true /* consult_young */)) {
if (Verbose && PrintGCDetails) {
gclog_or_tty->print("CMSCollector: collect because incremental collection will fail ");
}
return true;
}

if (MetaspaceGC::should_concurrent_collect()) {//检查metaspace是否需要GC
if (Verbose && PrintGCDetails) {
gclog_or_tty->print("CMSCollector: collect for metadata allocation ");
}
return true;
}

// CMSTriggerInterval starts a CMS cycle if enough time has passed.
//CMS的触发间隔,默认-1.
if (CMSTriggerInterval >= 0) {
if (CMSTriggerInterval == 0) {
// Trigger always
return true;
}

if (stats().cms_time_since_begin() >= (CMSTriggerInterval / ((double) MILLIUNITS))) {
if (Verbose && PrintGCDetails) {
if (stats().valid()) {
gclog_or_tty->print_cr("CMSCollector: collect because of trigger interval (time since last begin %3.7f secs)",
stats().cms_time_since_begin());
} else {
gclog_or_tty->print_cr("CMSCollector: collect because of trigger interval (first collection)");
}
}
return true;
}
}

return false;
}
//方法结束


这个方法比较长,主要有以下几种方式触发:

(1) 请求进行一次fullgc,如调用System.gc时

(2) 当没有设置UseCMSInitiatingOccupancyOnly时,会动态计算。如果完成CMS回收的所需要的预计的时间小于预计的CMS回收的分代填满的时间,就进行回收

(3) 调用should_concurrent_collect()方法返回true(后面会讲都有哪些情况时该方法会返回true)

(4) 如果预计增量式回收会失败时,也会触发一次回收。

(5) 如果metaSpace认为需要回收metaSpace区域,也会触发一次cms回收

(6) 如果设置了CMSTriggerInterval参数。该参数默认值-1,表示CMS回收的间隔。如果=0表示一直触发CMSGC。如果>0,则会判断从开始CMSGC到现在经过的时间是否大于该参数的时间。若大于,则会进行一次CMSGC。

如果都不满足,则返回false,表示不满足进行GC的条件。

再来看看第3条,看看什么时候should_concurrent_collect()会返回true。

bool ConcurrentMarkSweepGeneration::should_concurrent_collect() const {

assert_lock_strong(freelistLock());
if (occupancy() > initiating_occupancy()) {
if (PrintGCDetails && Verbose) {
gclog_or_tty->print(" %s: collect because of occupancy %f / %f  ",
short_name(), occupancy(), initiating_occupancy());
}
return true;
}
/*
如果使用UseCMSInitiatingOccupancyOnly参数,表示只有在使用率超过配置的情况下的时候
才会进行CMS回收。
如果没有使用-XX:+UseCMSInitiatingOccupancyOnly,
那么HotSpot VM只是利用这个值来启动第一次CMS垃圾回收,后面都是使用HotSpot VM自动计算出来的值。
*/
if (UseCMSInitiatingOccupancyOnly) {
return false;
}
if (expansion_cause() == CMSExpansionCause::_satisfy_allocation) {//扩容
if (PrintGCDetails && Verbose) {
gclog_or_tty->print(" %s: collect because expanded for allocation ",
short_name());
}
return true;
}
//这里的_cmsSpace是指CompactibleFreeListSpace
if (_cmsSpace->should_concurrent_collect()) {
if (PrintGCDetails && Verbose) {
gclog_or_tty->print(" %s: collect because cmsSpace says so ",
short_name());
}
return true;
}
return false;
}


可以发现,当

(1)堆当前的使用率>配置的initiating_occupancy,也就是我们经常配置的-XX:CMSInitiatingOccupancyFraction=80。

(2)当设置了UseCMSInitiatingOccupancyOnly=true,也就是只允许使用率达到阈值才进行回收时,不会判断其他条件。

(3)当堆扩容的原因是_satisfy_allocation时。

(4)CompactibleFreeListSpace认为需要回收时。

总结起来,就是下面这么多原因:

(1)请求进行一次fullgc,如调用System.gc时

(2)当没有设置UseCMSInitiatingOccupancyOnly时,会动态计算。如果完成CMS回收的所需要的预计的时间小于预计的CMS回收的分代填满的时间,就进行回收

(3.1)堆当前的使用率>配置的initiating_occupancy,也就是我们经常配置的-XX:CMSInitiatingOccupancyFraction=80。

(3.2)当设置了UseCMSInitiatingOccupancyOnly=true,也就是只允许使用率达到阈值才进行回收时,不会判断其他条件。

(3.3)当堆扩容的原因是_satisfy_allocation时。

(3.4)CompactibleFreeListSpace认为需要回收时。

(4)如果预计增量式回收会失败时,也会触发一次回收。

(5)如果metaSpace认为需要回收metaSpace区域,也会触发一次cms回收

(6)如果设置了CMSTriggerInterval参数。该参数默认值-1,表示CMS回收的间隔。如果=0表示一直触发CMSGC。如果>0,则会判断从开始CMSGC到现在经过的时间是否大于该参数的时间。若大于,则会进行一次CMSGC。

这里还需要说明的一点是-XX:CMSInitiatingOccupancyFraction配置的默认值是-1.当它是-1的时候,会通过concurrentMarkSweep.cpp中的init_initiating_occupancy(intx io, uintx tr)方法计算。

void ConcurrentMarkSweepGeneration::init_initiating_occupancy(intx io, uintx tr) {
//tr为CMSTriggerRatio参数,表示触发CMS的比例,默认80%,。IO表示CMSInitiatingOccupancyFraction,即使用率,默认-1
assert(io <= 100 && tr <= 100, "Check the arguments");
if (io >= 0) {
_initiating_occupancy = (double)io / 100.0;
} else {//所以默认没有配置的情况下,会走下面的逻辑,MinHeapFreeRatio默认40
_initiating_occupancy = ((100 - MinHeapFreeRatio) +
(double)(tr * MinHeapFreeRatio) / 100.0)
/ 100.0;
}
}


计算得到_initiating_occupancy=(100-40+80*40/100)/100=92%。也就是说,如果不配置CMSInitiatingOccupancyFraction参数,默认情况下,initiating_occupancy的值就是92%。

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

个人源码地址: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究竟对内存做了什么?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  jvm cms openjdk 源码 github