上午寫了一下環境介紹,下午接着將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機制。
附上源碼: 地址