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方法收尾,最後退出整個程序。 

    因此,我們可以說: 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機制。 

   附上源碼: 地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章