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

时间:2022-05-05 09:26:53

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址: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究竟对内存做了什么?

    
    上一篇文章中,我们说到如果在堆中分配失败,会通过VM触发一次由分配失败触发的一次GC。
    

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

    在讲VM_GenCollectForAllocation这个vm operation具体都做了什么前,先来看看VMThread是个什么东西。
    VMThread可以用以下命令查到。
    

jstack pid|grep VM

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

    
    在虚拟机创建时就会创建一个单例原生线程VMThread。这个线程能派生出其他线程。同时,这个线程的主要的作用是维护一个vm操作队列(VMOperationQueue),用于处理其他线程提交的vm operation,比如执行GC等。
    来看看VMOperationQueue:
    

class VMOperationQueue : public CHeapObj<mtInternal> {
private:
enum Priorities {
SafepointPriority, //0 高优(在safepoint下才能执行的操作)
MediumPriority, //1 中等
nof_priorities //2
};
/*
准确的说,像是数组+链表,跟map的结构有点像,不同链表是不同的优先级的VM操作
Priorities枚举类中的优先级分别是0,1,2的话,
_queue[0],_queue[1],_queue[2]就是3个链表,存着不同优先级的操作
*/

int _queue_length[nof_priorities];
int _queue_counter;
VM_Operation* _queue [nof_priorities];

    
    VMOperationQueue像是一个数组+链表的优先级队列,而不像我想象的java中的优先级队列一样,每次放入元素都要进行优先级的比较。那么每次取元素的时候怎么取呢?我们接着往下看。
    在VMThread的run()方法中,主要是调用了loop()方法,而loop()方法又调用了队列的remove_next()方法获取下一个需要处理的VM操作然后执行。VMThread的run()方法和loop()方法都比较长,比较复杂,在这就不贴代码了。来看看remove_next()方法。


VM_Operation* VMOperationQueue::remove_next() {
/*
假设vm操作队列是一个2级的优先队列。如果优先级的数量>2,我们需要不同的调度算法
*/

assert(SafepointPriority == 0 && MediumPriority == 1 && nof_priorities == 2,
"current algorithm does not work");

//避免低优先级线程饿死
int high_prio, low_prio;
if (_queue_counter++ < 10) {
high_prio = SafepointPriority;
low_prio = MediumPriority;
} else {
_queue_counter = 0;
high_prio = MediumPriority;
low_prio = SafepointPriority;
}

return queue_remove_front(queue_empty(high_prio) ? low_prio : high_prio);
}

    
    可以看到,前10次获取都会先获取SafepointPriority的操作。若SafepointPriority的队列非空,则先执行SafepointPriority的操作。超过后则优先取MediumPriority的操作,这样的避免低优先级的队列饿死。
    再来看看vmthread中比较关键的另外一个方法VMThread::execute(&op)中的关键部分:
    


if (op->evaluate_at_safepoint() && !SafepointSynchronize::is_at_safepoint()) {
SafepointSynchronize::begin();//驱使所有线程进入safepoint然后挂起他们
op->evaluate();//调用vm_operation的doit()方法进行回收。
SafepointSynchronize::end();//唤醒所有的线程,在safepoint执行之后,让这些线程重新恢复执行
} else {//如果已经在全局安全点,则可以操作vm操作了。
op->evaluate();
}

    execute()和run()方法都很复杂……只能截图比较关键的一部分说明。
    这里可以看到,当在执行一个vm操作时,先判断这个操作是否需要在safepoint下执行。若需要safepoint下执行且当前系统不在safepoint下,则调用SafepointSynchronize的方法驱使所有线程进入safepoint中,再执行vm操作。执行完后再唤醒所有线程。若此操作不需要在safepoint下,或者当前系统已经在safepoint下,则可以直接执行该操作了。所以,在safepoint的vm操作下,只有vm线程可以执行具体的逻辑,其他线程都要进入safepoint下并被挂起,直到完成此次操作。
    这里需要补充的一点是,如果vm发现一个需要safepoint的操作后面,跟着几个都需要safepoint的操作,它会进行特殊处理,使得这几个操作能接连执行,而不需要让所有线程停停开开(此部分代码在vmthread的loop方法中)。

    具体每个vm操作的处理逻辑,则是在每个operation覆盖父类的doit()方法中实现的。举例如VM_GenCollectForAllocation的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);//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();
}
}

    
    full gc的操作VM_GenCollectFull的doit()方法:
    

void VM_GenCollectFull::doit() {
SvcGCMarker sgcm(SvcGCMarker::FULL);

GenCollectedHeap* gch = GenCollectedHeap::heap();
GCCauseSetter gccs(gch, _gc_cause);
gch->do_full_collection(gch->must_clear_all_soft_refs(), _max_level);
}

    和GC相关的vm操作实现的doit()方法可查看该类:
https://github.com/FlashLightNing/openjdk-notes/blob/master/src/share/vm/gc_implementation/shared/vmGCOperations.cpp
    根据vm operation的阻塞类型以及是否需要进入安全点,vm操作分为以下4种模式

 class VM_Operation: public CHeapObj<mtInternal> {
public:
enum Mode {
_safepoint, // blocking, safepoint,
_no_safepoint, // blocking, no safepoint,
_concurrent, // non-blocking, no safepoint,
_async_safepoint // non-blocking, safepoint,
};

    了解哪些操作需要进入安全点对排查程序停顿时间过久等问题有帮助。通过调用evaluation_mode()可以得到某一个操作的模式。欲知所有的vm operation,可以查看
https://github.com/FlashLightNing/openjdk-notes/blob/master/src/share/vm/runtime/vm_operations.hpp

    文章写得比较杂,主要从内存分配失败后执行VM_GenCollectForAllocation的vm操作开始讲起,先写了管理vm操作的VMOperationQueue的构造,它是如何管理不同的优先级的vm操作,以及执行vm操作的vmthread是如何从队列中获取下一个需要执行的操作的,和vmthread的execute()方法的关键部分。最后简单介绍了一下vm操作的4种不同的模式和在doit()中实现不同操作的具体处理逻辑。
    

    从零开始看源码,旨在从源码验证书上的结论,探索书上未知的细节。有疑问欢迎留言探讨
    个人源码地址: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究竟对内存做了什么?