日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

main函数初探

發布時間:2023/12/20 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 main函数初探 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

main方法初探

    • 題外話
    • main函數的類是怎么被虛擬機識別加載至內存的
    • jvm的運行機制
    • jvm的啟動細節1---launch
    • jvm的啟動細節2---classloader

題外話

剛學java的同學肯定都知道main方法是一個程序的入口,為我們創建了一個主線程,作為一個老油條了,今天學習springboot項目啟動時發現也是通過main方法啟動的,于是就觸發了我的好奇心,main函數是怎么被執行的呢?被執行之前虛擬機幫我們做了些什么呢?

想了解springboot啟動流程和autoconfig的同學請看這里:

  • SpringBoot之@EnableAutoConfiguration注解
  • springboot autoconfig 自動配置原理實現
  • main函數的類是怎么被虛擬機識別加載至內存的

    java編譯成class文件后,虛擬機是怎么解析class文件并且初始化我們的實體類的呢? 這時候提到jvm類加載機制和雙親委派模型了

    1)Bootstrap ClassLoader

    負責加載$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++實現,不是ClassLoader子類

    2)Extension ClassLoader

    負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

    3)App ClassLoader

    負責記載classpath中指定的jar包及目錄中class

    4)Custom ClassLoader

    屬于應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規范自行實現ClassLoader

    加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。

    那么我們自己在項目里面寫的代碼是由哪個加載器加載的呢?

    雙親委派
    雙親委派模式要求除了頂層的啟動類加載器之外,其余的類加載器都應該有自己的父類加載器,但是在雙親委派模式中父子關系采取的并不是繼承的關系,而是采用組合關系來復用父類加載器的相關代碼。

    protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {// 增加同步鎖,防止多個線程加載同一類synchronized (getClassLoadingLock(name)) {// First, check if the class has already been loadedClass<?> c = findLoadedClass(name);if (c == null) {long t0 = System.nanoTime();try {if (parent != null) {c = parent.loadClass(name, false);} else { // ExtClassLoader沒有繼承BootStrapClassLoaderc = findBootstrapClassOrNull(name);}} catch (ClassNotFoundException e) {// ClassNotFoundException thrown if class not found// from the non-null parent class loader}if (c == null) {// If still not found, then invoke findClass in order// to find the class.long t1 = System.nanoTime();// AppClassLoader去我們項目中查找是否有這個文件,如有加載進來// 沒有就到用戶自定義ClassLoader中加載。如果沒有就拋出異常c = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}if (resolve) {resolveClass(c);}return c;} }

    工作原理
    如果一個類收到了類加載的請求,它并不會自己先去加載,而是把這個請求委托給父類加載器去執行,如果父類加載器還存在父類加載器,則進一步向上委托,依次遞歸,請求最后到達頂層的啟動類加載器,如果弗雷能夠完成類的加載任務,就會成功返回,倘若父類加載器無法完成任務,子類加載器才會嘗試自己去加載,這就是雙親委派模式。就是每個兒子都很懶,遇到類加載的活都給它爸爸干,直到爸爸說我也做不來的時候,兒子才會想辦法自己去加載。

    優勢
    采用雙親委派模式的好處就是Java類隨著它的類加載器一起具備一種帶有優先級的層次關系,通過這種層級關系可以避免類的重復加載,當父親已經加載了該類的時候,就沒有必要子類加載器(ClassLoader)再加載一次。其次是考慮到安全因素,Java核心API中定義類型不會被隨意替換,假設通過網路傳遞一個名為java.lang.Integer的類,通過雙親委派的的模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發現這個名字類,發現該類已經被加載,并不會重新加載網絡傳遞過來的java.lang.Integer.而之際返回已經加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。可能你會想,如果我們在calsspath路徑下自定義一個名為java.lang.SingInteger?該類并不存在java.lang中,經過雙親委托模式,傳遞到啟動類加載器中,由于父類加載器路徑下并沒有該類,所以不會加載,將反向委托給子類加載器,最終會通過系統類加載器加載該類,但是這樣做是不允許的,因為java.lang是核心的API包,需要訪問權限,強制加載將會報出如下異常。

    java.lang.SecurityException:Prohibited package name: java.lang

    想了解更多關于jvm類加載機制和雙親委派模型可以參考:

  • 深入理解JVM類加載機制
  • JVM內存結構、垃圾收集算法、垃圾收集器、字節碼文件結構、類加載機制
  • jvm的運行機制

    開始進入正題,讓我們一起看看底層C++代碼是怎么一步一步執行到main函數的吧

    main()方法:

    int main(int argc, char *argv[]) { //#ifdef DEBUGsync_wcout::set_switch(true); //#endifif (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參數設置
    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 backBytecodeEngine::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(); // deleteif (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 threadp.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 DEBUGsync_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 {//這里相當于觸發gcDWORD ret = raise(SIGINT);if (ret!=0) {assert(false);}}} }

    從它的方法名稱,以及實現來看,它應該是要 關閉除了當前線程之外的其它所有 jvm線程。可是一旦觸發信號后,實際上會導致程序整體退出。 我想這可能是 跟 main_exception_thread有關,那個方法會根據當前的線程,而定點關閉線程。 同時整個代碼塊是線程安全的。 這樣就是說,當SIGINT信號走到了 取消gc線程的時候,那么所有的線程一定是關閉了的。 bingo!!

    目前尚未驗證,但是我想應該是這樣的。 只是不知道這里并非根據線程去觸發 信號,應該是需要進一步完善的。

    目前為止,整個jvm的行為算是粗略的分析完了

    jvm的啟動細節1—launch

    節上文,jvm的launch方法的內容詳細講述一下。 在vm的launch中,有如下方法塊:

    .... 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); ....

    其中第一行代碼開辟的這個線程,便是java中的mian線程,也就是java中的主線程。 它接收命令的參數去執行任務。 其中,scapegoat方法 的代碼如下:

    //線程的任務 unsigned scapegoat (void *pp) {temp *real = (temp *)pp; // if (real->cur_thread_obj != nullptr) { // so the ThreadTable::get_thread_obj may be nullptr. // I add all thread into Table due to gc should stop all threads.ThreadTable::add_a_thread(GetCurrentThreadId(), real->cur_thread_obj, real->thread); // the cur_thread_obj is from `java/lang/Thread.start0()`. // }if (real->should_be_stop_first) { // if this thread is a child thread created by `start0`: should stop it first because of gc's race.// it will be hung up at the `global pthread_cond`. and will be wake up by `signal_all_thread()`.wait_cur_thread_and_set_bit(&real->the_first_wait_executed, real->thread);}real->thread->start(*real->arg);return 0; };

    通過代碼不難察覺,它首先將當前的線程加入了jvm的線程表中,進行管理(注意,此時jvm線程表中,實際上管理的線程有兩個了,一個是init_thread,它是本地線程的抽象,另一個就是 mian線程,也就是當前加入的這個線程)。 接著,當前線程(mian線程) 將會調用 start()方法,將命令行的參數一并傳遞。 start的源碼如下:

    void vm_thread::start(list<Oop *> & arg) {if (automan_jvm::inited() == false) {assert(method == nullptr); // if this is the init thread, method will be nullptr. this thread will get `main()` automatically.assert(arg.size() == 0);automan_jvm::inited() = true; // important!//todo: 這里是執行 main 方法 ,重要vm_thread::init_and_do_main(); // init global variables and execute `main()` function.} else {// [x] if this is not the thread[0], detach itself is okay because no one will pthread_join it.//todo: 這里分離子線程 // CloseHandle(tid); // pthread_detach(pthread_self());assert(this->vm_stack.size() == 0); // checkassert(arg.size() == 1); // run() only has one argument `this`.this->vm_stack.push_back(StackFrame(method, nullptr, nullptr, arg, this));this->execute();automan_jvm::num_lock().lock();{automan_jvm::thread_num() --;assert(automan_jvm::thread_num() >= 0);}automan_jvm::num_lock().unlock();}WaitForSingleObject(_all_thread_wait_mutex,INFINITE);this->state = Death;ReleaseMutex(_all_thread_wait_mutex); }

    它會根據jvm是否初始化而判定當前線程的start是去引導和啟動main方法,還是一般的線程。 注意我們在學習java的時候,實例化線程我們需要重寫run(){}方法,這個run方法里面寫的實際上是線程的任務,而線程的啟動,是由一個start0本地方法,即jvm調用的。 調用之后,會來到這里這個代碼塊。 它將走else下面這個代碼邏輯,里面實際上就是執行了run方法。 run方法執行完后,會將當前的線程狀態改為 death. 之后這個線程便會在特定的時期被gc給回收掉。

    扯遠了,我們還是看看main線程的操作吧。 它將會調用 vm_thread::init_and_do_main()方法。 這個方法就比較長,我將分塊展示。

    1.初始化Class,用于類的映射

    java_lang_class::init(); // must init !!!auto class_klass = BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/Class");java_lang_class::fixup_mirrors(); // only [basic types] + java.lang.Class + java.lang.Object

    首先,調用java_lang_class::init()方法,它的作用是將 使用標識符與 (核心)類進行隱射,代碼如下:

    void java_lang_class::init() { // must execute this method before jvm!!!auto & delay_mirrors = get_single_delay_mirrors();// basic types.delay_mirrors.push(L"I");delay_mirrors.push(L"Z");delay_mirrors.push(L"B");delay_mirrors.push(L"C");delay_mirrors.push(L"S");delay_mirrors.push(L"F");delay_mirrors.push(L"J");delay_mirrors.push(L"D");delay_mirrors.push(L"V"); // void...delay_mirrors.push(L"[I");delay_mirrors.push(L"[Z");delay_mirrors.push(L"[B");delay_mirrors.push(L"[C");delay_mirrors.push(L"[S");delay_mirrors.push(L"[F");delay_mirrors.push(L"[J");delay_mirrors.push(L"[D");// set statestate() = Inited; }

    然后,使用BootstrapClassLoader去加載 Class類,注意注意,我們看下BoostrapClassLoader的源碼:

    class BootStrapClassLoader : public ClassLoader { private:JarLister jl; private:BootStrapClassLoader() {}BootStrapClassLoader(const BootStrapClassLoader &);BootStrapClassLoader& operator= (const BootStrapClassLoader &);~BootStrapClassLoader() {} public:static BootStrapClassLoader & get_bootstrap() {static BootStrapClassLoader bootstrap;return bootstrap;} // singletonKlass *loadClass(const wstring & classname, ByteStream * = nullptr, MirrorOop * = nullptr,bool = false, InstanceKlass * = nullptr, ObjArrayOop * = nullptr) override;void print() override;void cleanup() override; };

    從接口中,我們能夠判斷兩條消息:

    get_bootstrap調用會得到一個單例對象;
    BootstrapClassLoader有一個成員變量JarLister,首次調用時,會觸發它的構造方法,我們去看看它的構造方法:

    2.初始化BootstrapClassloader以及加載Class:

    //todo: 修改 rjd 為windows 的路徑 JarLister::JarLister() : rjd(L"") {pwd = utf8_to_wstring(getProgramDir());rjd = RtJarDirectory(pwd);wstring rtjar_folder; #if (defined (__APPLE__))rtjar_folder = utf8_to_wstring(pt.get<std::string>("path.mac")); #elif (defined (__linux__))rtjar_folder = utf8_to_wstring(pt.get<std::string>("path.linux")); #else//todo: 這里配置 windows的 rt路徑rtjar_folder = utf8_to_wstring("C:\\Program Files\\Java\\jdk1.8.0_161\\jre\\lib\\"); #endifrtjar_pos =L"\""+ rtjar_folder + L"rt.jar"+L"\"";// copy lib/currency.data to ./lib/currency.data ......wstringstream ss;int status = system(wstring_to_utf8(ss.str()).c_str());if (status == -1) { // http://blog.csdn.net/cheyo/article/details/6595955 [shell 命令是否執行成功的判定]std::cerr << "system error!" << endl;}bool success = this->getjarlist(rtjar_pos);if (!success) exit(-1);ifstream f(wstring_to_utf8(this->rtlist), std::ios_base::in);std::string s;while(!f.eof()) {f >> s; // 這里有一個細節。因為最后一行僅僅有個回車,所以會讀入空,也就是 s 還是原來的 s,即最后一個名字被讀入了兩遍。使用其他的方法對效率不好,因此在 add_file 中解決了。如果檢測到有,忽略。if (!Filter::filt(utf8_to_wstring(s))) {this->rjd.add_file(StringSplitter(utf8_to_wstring(s)));}} }

    通過代碼可以分析,它做了這樣的事情:

  • 或許到當前環境的 rt.jar,這個文件時jvm的核心jar包,我配置的是我本機環境的rt.jar,它的版本號是: jdk_1.8_161。同時,因為原來這里使用了配置的方式,但是需要以來boost,我就給替換了,直接手寫死的。

  • 調用 getjartlist方法,該方法馬上詳述。

  • 調用rjd的add_file方法。 也將會詳述。

    下面是getjarlist方法:

  • /*===---------------- JarLister --------------------*/ bool JarLister::getjarlist(const wstring & rtjar_pos) const {wstringstream cmd;cmd << L"jar tf " << rtjar_pos << L" > " << this->rtlist;int status = system(wstring_to_utf8(cmd.str()).c_str());if (status == -1) {exit(-1);}// TODO: judge whether mkdir is exist?if (0==access(wstring_to_utf8(uncompressed_dir).c_str(),F_OK)) { // 如果存在return true;}cmd.str(L"");cmd << L"mkdir " << uncompressed_dir;status = system(wstring_to_utf8(cmd.str()).c_str());if (status == -1) {exit(-1);}cmd.str(L"");std::wcout << "unzipping rt.jar from: [" << rtjar_pos << "] ... please wait.\n";cmd << L"unzip " << rtjar_pos << L" -d " << uncompressed_dir ;status = system(wstring_to_utf8(cmd.str()).c_str());if (status == -1) { // http://blog.csdn.net/cheyo/article/details/6595955 [shell 命令是否執行成功的判定]std::cerr << "system error!" << endl;exit(-1);} else {if (status) {if (0 ==status) {std::wcout << "unzipping succeed.\n";return true;}else {std::cerr << "Your rt.jar file is not right!" << endl;}} else {std::cerr << "other fault reasons!" << endl;}}return false; }

    它做了這樣的事情:

    1.通過jar tf 將rt.jar保存的所有類 保存至某一特定文件中;

    2.將rt.jar解壓至某一個特定文件夾中;(注意,這個文件夾將會作為是否解壓的標準,在同一進程中,最多只可能被加壓一次。 我當前版本加壓出來有 2999個類,如果解壓信息輸出到控制臺的話,還是要費點時間。但是我想在發行版中,一個環境下的jvm,應該只被解壓一次)。

    接著是rtjarDirectory的addfile()方法:

    void RtJarDirectory::add_file(StringSplitter && ss) {if (ss.counter() == 0) { // 僅僅在第一次的時候做檢查,看文件到底存不存在if (this->find_file(std::move(ss)) == true) return;else ss.counter() = 0;}const wstring& target = ss.result()[ss.counter()];if (ss.counter() == ss.result().size() - 1) { // next will be the target, add.subdir->insert(make_shared<RtJarDirectory>(target));} else { // dir.auto next_dir = findFolderInThis(target);ss.counter() += 1;if (next_dir != nullptr) {(*next_dir).add_file(std::move(ss)); // delegate to the next level dir.} else { // no next_dir, we should create.// this level's `subdir` can't be nullptr :)subdir->insert(make_shared<RtJarDirectory>(target));next_dir = findFolderInThis(target);assert(next_dir != nullptr);(*next_dir).add_file(std::move(ss));}} }

    它會將 保存的所有的類,通過根據全限定名稱(包名+類名)的形式,進行分割,最終將 類名(去除了包名)的名稱 保存進一個智能指針。 這個結構頗為復雜,我沒太看懂,它的定義是這樣的:

    shared_ptr<set<shared_ptr<RtJarDirectory>,shared_RtJarDirectory_compare>> subdir; //sub directory

    我暫且先理解為它保存了所有的類名稱在內存中吧,注意到我獲取的類的條目有一萬多:

    BootstrapClassLoader初始化完成后,就該去loadClass了,以加載Class為例: // add lock simplyLockGuard lg(system_classmap_lock);assert(jl.find_file(L"java/lang/Object.class")==1);wstring target = classname + L".class";if (jl.find_file(target)) {if (system_classmap.find(target) != system_classmap.end()) { // has been loadedreturn system_classmap[target];} else { // load// parse a ClassFile (load)ifstream f(wstring_to_utf8(jl.get_sun_dir() + L"/" + target).c_str(), std::ios::binary);if(!f.is_open()) {std::wcerr << "wrong! --- at BootStrapClassLoader::loadClass" << std::endl;exit(-1);} #ifdef DEBUGsync_wcout{} << "===----------------- begin parsing (" << target << ") 's ClassFile in BootstrapClassLoader..." << std::endl; #endifClassFile *cf = new ClassFile;ClassFile_Pool::put(cf);f >> *cf; #ifdef DEBUGsync_wcout{} << "===----------------- parsing (" << target << ") 's ClassFile end." << std::endl; #endif// convert to a MetaClass (link)InstanceKlass *newklass = new InstanceKlass(cf, nullptr);system_classmap.insert(make_pair(target, newklass)); #ifdef KLASS_DEBUGBootStrapClassLoader::get_bootstrap().print();MyClassLoader::get_loader().print(); #endifreturn newklass;}//todo:equals to starts with} .........

    上面為loadClass代碼片段,可以看到第二行,它首先判斷了java.lang.Object.class,通過前面的那個智能指針,顯然Object是在里面的。(但是,此時Object并未加載。)這也印證了我前天文章中所說的,Object的唯一性是必須要首先保證的。 同時,加載成功時,會首先 實例一個ClassFile放入 類池,這個類對象保存的是 字節碼二進制流!!! 接著,實例化一個 實例類對象,將該類對象保存進入 system_classmap。

    3.設置Object,以及基本類型的單例映像:
    該步驟是通過 Class完成的。

    java_lang_class::fixup_mirrors(); // only [basic types] + java.lang.Class + java.lang.Object

    該方法將 8個基本類型,和void 以Mirror實例的形式放入了一個緩存。

    ...... switch (name[0]) {case L'I':case L'Z':case L'B':case L'C':case L'S':case L'F':case L'J':case L'D':case L'V':{ // include `void`.// insert into.MirrorOop *basic_type_mirror = ((MirrorKlass *)klass)->new_mirror(nullptr, nullptr);basic_type_mirror->set_extra(name); // set the name `I`, `J` if it's a primitve type.get_single_basic_type_mirrors().insert(make_pair(name, basic_type_mirror));break;}default:{assert(false);}} ......

    4.加載String及Thread:

    // load String.classauto string_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/String"));// 1. create a [half-completed] Thread obj, using the ThreadGroup obj.(for currentThread(), this must be create first!!)auto thread_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/Thread")); InstanceOop *init_thread = thread_klass->new_instance();BytecodeEngine::initial_client(thread_klass, *this); // first <clinit>!// inject!!//todo: 注意這里的 threadid 的來源,可能要修改init_thread->set_field_value(THREAD L":eetop:J", new LongOop((uint64_t)GetCurrentThreadId()));//todo: 這里的線程優先級還沒有綁定到 thread句柄上, 通過 setThreadPriorityinit_thread->set_field_value(THREAD L":priority:I", new IntOop(5));//todo: 這里通過 openthread 根據當前線程的id 獲取到線程句柄 注意,當前線程的句柄不能關閉ThreadTable::add_a_thread(GetCurrentThreadId(), init_thread, this);

    加載完Thread之后,會生成一個Thread的實例對象。之后會進行類的初始化,注意這是jvm中第一次類的初始化,它會一直向上初始化直到Object。

    設置相關屬性后,放入jvm的線程表中。 注意了,此時有在線程表中,就有了兩個個線程了。 但是注意這個線程跟第二個線程的id號是一致的,同時,它并不是真正意義上的線程,只是一個抽象。 在線程表的插入中有如下代碼:

    if (get_thread_table().insert(make_pair(tid, make_tuple(get_thread_table().size(), _thread, t))).second == false) { // 如果原先已經插入了的話,那么就復用原先的 thread_no.number = std::get<0>(get_thread_table()[tid]);}

    也就是說,the_whole_world的線程表中的線程數仍然是兩個。

    5.加載并實例化ThreadGroup:

    // 2. create a [System] ThreadGroup obj.auto threadgroup_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/ThreadGroup"));InstanceOop *init_threadgroup = threadgroup_klass->new_instance();BytecodeEngine::initial_client(threadgroup_klass, *this); // first <clinit>!{std::list<Oop *> list;list.push_back(init_threadgroup); // $0 = this// execute method: java/lang/ThreadGroup.<init>:()V --> private Method!!Method *target_method = threadgroup_klass->get_this_class_method(L"<init>:()V");assert(target_method != nullptr);this->add_frame_and_execute(target_method, list);} // 3. INCOMPLETELY create a [Main] ThreadGroup obj.InstanceOop *main_threadgroup = threadgroup_klass->new_instance();{init_thread->set_field_value(THREAD L":group:Ljava/lang/ThreadGroup;", main_threadgroup);}assert(this->vm_stack.size() == 0);BytecodeEngine::initial_client(((InstanceKlass *)class_klass), *this);((InstanceKlass *)class_klass)->set_static_field_value(L"useCaches:Z", new IntOop(false));

    注意,這里除了 類初始化之外,還初始化了一個 ThreadGroup對象。 實例化的threadGroup為主線程組。

    6.加載并初始化System:

    // 3. load System classauto system_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/System"));system_klass->set_state(Klass::KlassState::Initializing); // BytecodeEngine::initial_clinit(system_klass, *this);auto InputStream_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/io/InputStream"));BytecodeEngine::initial_client(InputStream_klass, *this);auto PrintStream_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/io/PrintStream"));BytecodeEngine::initial_client(PrintStream_klass, *this);auto SecurityManager_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/SecurityManager"));BytecodeEngine::initial_client(SecurityManager_klass, *this);

    System用于設置相關的屬性,并且完成 當前程序的標準輸入流,輸出流,錯誤流的綁定。 windows上,每個程序的標準輸入輸出和錯誤流就是控制臺。

    7.加載并實例化Perf 和 LauncherHelper:

    auto Perf_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"sun/misc/Perf"));Perf_klass->set_state(Klass::KlassState::Initializing); // ban Perf.auto PerfCounter_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"sun/misc/PerfCounter"));PerfCounter_klass->set_state(Klass::KlassState::Initializing); // ban PerfCounter.auto launcher_helper_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"sun/launcher/LauncherHelper"));BytecodeEngine::initial_client(launcher_helper_klass, *this);Method *load_main_method = launcher_helper_klass->get_this_class_method(L"checkAndLoadMain:(ZILjava/lang/String;)Ljava/lang/Class;");

    Perf作用暫時不清楚,LauncherHelper會用于引導 Launcher然后使用 AppClassLoader去加載用戶類。

    8.加載動態調用的類:

    // load some useful klass...{auto methodtype_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/invoke/MethodType"));BytecodeEngine::initial_client(methodtype_klass, *this);auto methodhandle_klass = ((InstanceKlass *)BootStrapClassLoader::get_bootstrap().loadClass(L"java/lang/invoke/MethodHandle"));BytecodeEngine::initial_client(methodhandle_klass, *this);}

    9.執行main:
    launch流程結束。

    jvm的啟動細節2—classloader

    說到這個古老的話題,那可得追溯到我寫文章開始。 那一年春節,我決定看看源碼,一上來就猛的 想把那個雙親加載機制給搞明白。 事實上,一直沒怎么搞明白,唯一的作用是給了自己學習的動力。 但是今天我想借著這個機會,是可以完全搞懂的。

    上午說到,jvm在加載客戶類之前,會啟動一個LauncherHelper類,代碼如下:

    BytecodeEngine::initial_client(launcher_helper_klass, *this);Method *load_main_method = launcher_helper_klass->get_this_class_method(L"checkAndLoadMain:(ZILjava/lang/String;)Ljava/lang/Class;");// new a String.wstring ss = automan_jvm::main_class_name();InstanceOop *main_klass = (InstanceOop *)java_lang_string::intern(automan_jvm::main_class_name());this->vm_stack.push_back(StackFrame(load_main_method, nullptr, nullptr, {new IntOop(true), new IntOop(1), main_klass}, this)); //todo: 到這里應該是java層面的類加載器開始生效!!! MirrorOop *main_class_mirror = (MirrorOop *)this->execute(); 接著去看看 checkAndLoadMain方法:

    之后通過ClassLoader的loadClass方法,通過查找虛擬表,調用Launcher的AppClassLoader的loadClass方法,再調用之前,將會首先進行AppClassClassloader的初始化(這個初始化過程蠻復雜的):




    花了較多的精力去看這塊的源碼,主要原因基于以下幾點:

    1.windows中由于文件系統路徑分隔符的原因,我在調試時就遇到了一個坑,即我明明需要使用 文件系統去加載,但是它卻使用了JarLoader去加載。 現在回過頭來,知道了其原因: 首先在LauncherHelper中會根據模式選擇加載器,其次會判斷java.class.path下面的所有配置路徑,如果為文件夾,則會匹配為 fileLoader,如果為文件,則會使用默認的loader,而實際上默認的loader,其最終采用的還是Jarloader的方式加載的。(我的問題就出在這!)。

    2.AppClassLoader與ExtClassLoader都繼承自URLClasspath,因此必須弄明白URLClasspath是在什么時機初始化的,以及相應的參數都是什么。 關于URLClasspath的作用,網上帖子挺多。 它的幾個關鍵方法: loadClass,findClass,defineClass 可以用于驗證雙親委派機制的運行流程。

    我在調試時,分別給loadClass 和 defineClass添加了錨點, 最終實際上是可以印證雙親委派模型的。 由于當時未截圖,因此這里就不再繼續操作了。(因為這個過程實在是有些痛苦~,感興趣的小伙伴可以親自調試一下)。

    另外強調一點就是,以前沒有認識到URLClassLoader在java中的重要性,以及URLClassLoader與URLClasspath的關系,這是一個十分有趣的問題。

    此外,在AppclassLoader加載類的過程中,它的流將會以匿名內部類的形式給出:

    就到這吧,原計劃寫的很詳細的,但是真寫了太細了吧,速度太慢,自己又是個急性子。 后續再補充細節,還需要進一步的學習補充。。。

    總結

    以上是生活随笔為你收集整理的main函数初探的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。