windows系统使用c++实现一个小型jvm(二)------------jvm的运行机制

  上午写了一下环境介绍,下午接着将jvm的运行机制给记录一下。 我将从源码角度,进行分析,一步步的将一个java程序的生到死进行梳理。

  需要注意,启动程序的时候,需要带一个参数,该参数为 当前需要执行 class文件,里面需要包含mian()方法。  当然了,这是其中一种的类启动方式,还有一种jar启动方式,我将在后文进行分析。 

  当前环境下,我是指定了一个 helloworld程序,在当前目录的下,这个我代码里面是写死的,当然后面可以通过修改代码达到一个配置的效果。 接着看看 c++层面的 启动方法,代码如下:

main()方法:

int main(int argc, char *argv[])
{
//#ifdef DEBUG
    sync_wcout::set_switch(true);
//#endif
    if (argc != 2) {
        std::wcerr << "argc is not 2. please re-run." << std::endl;
        exit(-1);
    }
    wstring program = utf8_to_wstring(std::string(argv[1]));
    std::ios::sync_with_stdio(true);		// keep thread safe?
    std::wcout.imbue(std::locale(""));
    std::vector<std::wstring> v{ L"automan_jvm", L"1", L"2" };
    automan_jvm::run(program, v);
}

   从源码中,我们可以看到,它从程序启动时获取参数,并进行监测。  目前该程序要求的是只能有两个参数。 但是实际上还有许多参数是可以在这里配置的,比如:这篇文章: jvm参数汇总 所记录的参数。当然了,这些参数在本demo的支持程度有待考究。 

run()方法:

    然后,它以相关的参数,调用了 jvm的 run方法。  代码如下:

void automan_jvm::run(const wstring & main_class_name, const vector<wstring> & argv)
{
    //todo: 这里注册了一个交互信号,信号处理程序又是一个 无限循环,需要注意
    //todo: 用 raise 生成信号
    signal(SIGINT, SIGINT_handler);
    automan_jvm::main_class_name() = std::regex_replace(main_class_name, std::wregex(L"\\."), L"/");
    automan_jvm::argv() = const_cast<vector<wstring> &>(argv);
    vm_thread *init_thread;
    automan_jvm::lock().lock();
    {
        automan_jvm::threads().push_back(vm_thread(nullptr, {}));
        init_thread = &automan_jvm::threads().back();
    }
    automan_jvm::lock().unlock();
    init_native();
    HANDLE gc_tid;
    gc_tid= (HANDLE)(_beginthreadex(nullptr, 0, reinterpret_cast<unsigned int (*)(void *)>(GC::gc_thread), NULL, 0, NULL));
    gc_thread() = gc_tid;
    // go!
    init_thread->launch();		// begin this thread.
}

   注意到代码的第二行,它注册了一个交互信号,此时的环境仍然是本地线程,即该线程为根线程,不受jvm的管理。信号处理程序实际上是一个 gc任务,这也是为什么我们时常听到java的gc触发,既有jvm管理的部分,也有本地的部分。 它的代码如下:

void SIGINT_handler(int signo)
{
    // re-use gc bit to stop-the-world,but won't trigger GC。
    while (true) {
        bool gc;
        GC::gc_lock().lock();
        {
            gc = GC::gc();
        }
        GC::gc_lock().unlock();
        if (gc) {
            continue;
        } else {
            GC::gc_lock().lock();
            {
                GC::gc() = true;
            }
            GC::gc_lock().unlock();
            // FIXME: I don't know whether it is safe... only a solution for dead lock of wind_jvm::num_lock...
            automan_jvm::num_lock().unlock();		// It's only a patch.
            GC::detect_ready();
            GC::gc() = false;		// set back
            BytecodeEngine::main_thread_exception();		// exit
        }
    }
}

   我们观察到,该方法实际上是一个无限循环,但是这个线程又是根线程,所以我最开始有一些疑惑。 现在回头来看时,我注意到: GC::detect_ready(),该方法实际上也是无限循环的,但是,它会主动的释放cpu。 我们看看这个方法:

void GC::detect_ready()
{
    while (true) {
        LockGuard lg(gc_lock());
        int total_ready_num = 0;
        int total_size;
        ThreadTable::get_lock().lock();
        {
            total_size = ThreadTable::get_thread_table().size();
            for (auto & iter : ThreadTable::get_thread_table()) {
                thread_state state = std::get<2>(iter.second)->state;
                if (state == Waiting || state == Death/*iter.second == false && iter.first->vm_stack.size() == 0*/) {
                    total_ready_num ++;
                } else {
                    break;
                }
            }
        }
        ThreadTable::get_lock().unlock();
        ThreadTable::print_table();		// delete
        if (total_ready_num == total_size) {		// over!
            return;
        }
        Sleep(1);
    }
}

   可以看到,它实际上在管理 jvm内部的线程。 根据线程状态,进行线程的回收。 它的退出条件是: jvm的内部所有线程均为活跃线程。  同时,它没有互斥量进行阻塞,可见它的活跃程度是很高的。  换言之,一旦程序触发了退出信号,jvm内部的线程维护,几乎时刻在运行。 

  让我们回到 信号处理程序上,它最后调用了: BytecodeEngine::main_thread_exception(),它的代码及作用如下:

void BytecodeEngine::main_thread_exception(int exitcode)		// dummy is use for BytecodeEngine::excute / SIGINT_handler.
{
    automan_jvm::lock().lock();
    {
        for (auto & thread : automan_jvm::threads()) {
            WaitForSingleObject(_all_thread_wait_mutex,INFINITE);
            thread_state state = thread.state;
            ReleaseMutex(_all_thread_wait_mutex);
            if (state == Death) {					// pthread_cancel SIGSEGV bug sloved:
                continue;
            }
            if (thread.tid != GetCurrentThreadId()) {
                HANDLE _handle =OpenThread(THREAD_ALL_ACCESS,FALSE,GetCurrentThreadId());
                WaitForSingleObject(_handle,INFINITE);
                CloseHandle(_handle);
                //todo: 当线程执行完后,清理。
                cleanup(nullptr);
            } else {
                thread.state = Death;
            }
        }
    }
    automan_jvm::lock().unlock();
    GC::cancel_gc_thread();
    automan_jvm::end();
    exit(exitcode);
}

   可以看到,它实际上是等待当前jvm中,所有的线程执行完毕,回收相关的资源。 然后回收gc线程,调用jvm的end方法收尾,最后退出整个程序。 

扫描二维码关注公众号,回复: 10657826 查看本文章

    因此,我们可以说: jvm启动之初,挂载了一个信号处理程序,该程序负责所有的收尾工作,一旦接收到特定信号,整个程序完成,然后退出。 但是这里我一点不懂,它设计成了 while(true)的方式,但是实际上该代码块只能被执行一次,这个问题留待以后有缘再回答吧。 

   ok,如今我可以回退退退到: jvm的run方法那里,继续解读。 

   信号处理程序注册完后,之后的代码功能依次是: 

         1.将传入的参数保存到jvm中; 

         2.在jvm的线程表中,插入了一个方法和参数均为空的线程,注意了,该线程将被称为初始化线程(init_thread),并且该线程不受 jvm的管控,它是本地线程象征性的放入 jvm的线程表

        3.初始化本地方法,实际上就是将本地的库地址进行缓存本质上将本地的方法缓存起来,缓存的内容包括native包下的所有类的核心方法。

       4.开启一个gc线程,同时该gc线程并不会放入 jvm的线程表中,而是单独的存储在jvm中。也就是jvm可以直接操纵该线程。 此外,这个gc线程本身也是一个真正意义上的线程,它才生成之后,将会根据信号,阻塞式的进行垃圾清理。它与上面注册的那个信号处理程序有些不同,我们可以看到其源码:

unsigned *GC::gc_thread(void *)
{
    // init `cond` and `mutex` first:
    gc_cond = CreateEvent(NULL,FALSE,FALSE,NULL);
    gc_cond_mutes=CreateMutex(NULL,FALSE,NULL);
    while (true) {
        WaitForSingleObject(gc_cond_mutes,INFINITE);
        //todo: 这里会等待gc条件,该线程具有跟进程一样长的生命周期
        WaitForSingleObject(gc_cond,INFINITE);
        ReleaseMutex(gc_cond_mutes);
        detect_ready();
        system_gc();
    }
}

   它会阻塞式的接收处理信号,每次任务,将会首先处理线程回收,然后处理资源回收,也就是system_gc()的作用,考虑到这里主线不是讨论gc,所以暂时先不看gc的细节。 

  5.初始化线程调用launch()操作,进行java程序的启用,需要注意,当前的初始化线程(init_thread),也就是根线程。 

launch()方法:

   该方法可以说是关键了,内容很细也很多,考虑到本文的主线任务,将略写本方法,提一提它的功能作用即可,其代码如下: 

void vm_thread::launch(InstanceOop *cur_thread_obj)
{
    // start one thread
    p.thread = this;
    p.arg = &const_cast<std::list<Oop *> &>(arg);
    p.cur_thread_obj = cur_thread_obj;
    if (cur_thread_obj != nullptr) {		// if arg is not nullptr, must be a thread created by `start0`.
        p.should_be_stop_first = true;
    }
    bool inited = automan_jvm::inited();
    //todo: 实际上这个线程是用于初始化的
   HANDLE cur_handle = (HANDLE)(_beginthreadex(NULL, 0, scapegoat, &p, 0, NULL));
    this->tid = GetThreadId(cur_handle);		// save to the vm_thread.
    if (!inited) {		// if this is the main thread which create the first init --> thread[0], then wait.
        //todo: 阻塞执行 tid线程,tid执行完后才往后执行
        WaitForSingleObject(cur_handle,INFINITE);
        GC::signal_all_patch();
        int remain_thread_num;
        while(true) {
            automan_jvm::num_lock().lock();
            {
                remain_thread_num = automan_jvm::thread_num();
            }
            automan_jvm::num_lock().unlock();

            assert(remain_thread_num >= 0);
            if (remain_thread_num == 0) {
                break;
            }
            //让出CPU调度
            Sleep(0);
        }
        GC::cancel_gc_thread();
        automan_jvm::end();
#ifdef DEBUG
        sync_wcout{} << pthread_self() << " run over!!!" << std::endl;		// delete
#endif
    }
}

   从代码中可以看出,它首先将 init_thread与jvm进行了绑定,然后获取jvm的初始化状态。当然了首次运行时,此时其肯定未被初始化。  之后它将开启另一个线程,注意注意了,这是继 gc线程后,本程序开启的第二个线程。 这个线程实际上将是我们在java端调用mian方法的那个线程。同时,它也会做很多的工作,在本文中,我先暂不讨论,后文会专门的讨论。 

   之后,init_thread将会阻塞在此,直到java的mian线程结束。  之后init_thread会做如下工作:

    1.唤醒所有阻塞的线程。 其代码如下:

void GC::signal_all_patch()
{
    while(true) {
        gc_lock().lock();
        if (!GC::gc()) {		// if not in gc, signal all thread is okay.
            signal_all_thread();
            break;
        }
        gc_lock().unlock();
    }
    gc_lock().unlock();
}
void signal_all_thread()
{
   int size = automan_jvm::thread_num();
    for (int i = 0; i < size; ++i) {
        SetEvent(_all_thread_wait_cond);
        //todo: 这里通过释放 CPU 达到broadst的目的
        Sleep(0);
    }
}

    2.判断当前jvm的线程数量,当线程数量为0的时候,退出循环。

   3.取消gc线程,以及 调用 jvm的end进行收尾。 整个程序代码执行完毕,退出。 这里我需要提一下,windows中,主线程退出,则子线程也会立即退出,无论子线程是否执行完毕(linux则不会)。   所以,这里面对jvm 的稳健性有要求,如果jvm错误的判断当前系统中的线程数,则会造成一个不可预见的后果。 (注意,当java的主线程执行完毕后,jvm实际上进入了一个预退出状态,此时对线程的管理是十分活跃的,正如 信号处理程序中的那样!我不知道这是整个demo本身的原因,还是说发行版的jvm就是这样设计的。)

总结: 

   目前,我们知道,整个jvm的原生阶段(不考虑java中新开线程的影响),包含了三个线程,即初始化线程gc线程java的主线程。 

   程序的正常退出包括两个途径:

        1.java的mian线程执行完毕后,且jvm的其它线程均执行完毕,则整个程序会因为代码执行完毕而退出。  

        2.触发了退出的信号,我查了下信号量: SIGINT, 它好像是 "通过ctrl+c对当前进程发送结束信号",这就说得通了。 

        在代码中, 我找到有主动发出这个信号的地方,位于runtime/thread中,但是pthread中,是给单个线程发送信号,在windows中,我暂未找到相关的api,因此就是粗略处理的: 其代码如下: 

void ThreadTable::kill_all_except_main_thread(DWORD main_tid)
{
    for (auto iter : get_thread_table()) {
        if (iter.first == main_tid)	continue;
        else {
            //这里相当于触发gc
          DWORD ret = raise(SIGINT);
            if (ret!=0) {
                assert(false);
            }
        }
    }
}

   从它的方法名称,以及实现来看,它应该是要 关闭除了当前线程之外的其它所有 jvm线程。可是一旦触发信号后,实际上会导致程序整体退出。 我想这可能是 跟 main_exception_thread有关,那个方法会根据当前的线程,而定点关闭线程。 同时整个代码块是线程安全的。  这样就是说,当SIGINT信号走到了 取消gc线程的时候,那么所有的线程一定是关闭了的。   bingo!!

   目前尚未验证,但是我想应该是这样的。  只是不知道这里并非根据线程去触发 信号,应该是需要进一步完善的。 


   目前为止,整个jvm的行为算是粗略的分析完了,后文将仔细分析jvm的launch流程,类加载机制,gc机制。 

   附上源码: 地址

发布了340 篇原创文章 · 获赞 159 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/qq_36285943/article/details/104714470