[Inside HotSpot] hotspot的啟動流程與main方法調用


hotspot的啟動流程與main方法調用

虛擬機的使命就是執行public static void main(String[])方法,從虛擬機創建到main方法執行會經過一系列流程。這篇文章詳細討論了執行命令行java.exe HelloWorld調用main函數輸出經歷了什么。源碼使用openjdk12,操作系統為windows 64bits,其它系統和源碼版本大同小異。

java.base

首先要明白一個概念,java.exe大體上可以分為啟動器部分和hotspot部分。
啟動器負責執行一些命令行解析,環境初始化等任務,hotspot部分則是真正的虛擬機干活的地方。

啟動器是用C++寫的,如果不修改鏈接器入口點名字,執行java.exe xxx追根溯源必然會跟蹤到main函數。這個main位於
openjdk12\src\java.base\share\native\launcher\main.c,這里就是java啟動器的最終起源了:

#ifdef JAVAW

char **__initenv;

int WINAPI
WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)
{
    int margc;
    char** margv;
    int jargc;
    char** jargv;
    const jboolean const_javaw = JNI_TRUE;

    __initenv = _environ;

#else /* JAVAW */
JNIEXPORT int
main(int argc, char **argv)
{
    int margc;
    char** margv;
    int jargc;
    char** jargv;
    const jboolean const_javaw = JNI_FALSE;
#endif /* JAVAW */

    // 處理傳遞給啟動器的參數

    return JLI_Launch(margc, margv,
                   jargc, (const char**) jargv,
                   0, NULL,
                   VERSION_STRING,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   jargc > 0,
                   const_cpwildcard, const_javaw, 0);
}

如果用戶執行的是javaw.exe就進入WinMain入口,否則java.exe進入main入口。

在main中會處理啟動器的參數比如這種-XX:+UnlockDiagnosticVMOptions -XX:+PauseAtExit ,處理完之后調用JLI_Launcher。多說一點,啟動器代碼也分為系統相關和系統無關,像java.base/linux,java.base/windows這種就是平台相關,java.base/share就是平台無關代碼。

java.base -> JLI_Launcher

JLI_Launcher位於openjdk12\src\java.base\share\native\libjli\java.c

JNIEXPORT int JNICALL
JLI_Launch(int argc, char ** argv,              /* main argc, argc */
        int jargc, const char** jargv,          /* java args */
        int appclassc, const char** appclassv,  /* app classpath */
        const char* fullversion,                /* full version defined */
        const char* dotversion,                 /* dot version defined */
        const char* pname,                      /* program name */
        const char* lname,                      /* launcher name */
        jboolean javaargs,                      /* JAVA_ARGS */
        jboolean cpwildcard,                    /* classpath wildcard */
        jboolean javaw,                         /* windows-only javaw */
        jint     ergo_class                     /* ergnomics policy */
);

JLI_Launcher做了很多重要的事情

  1. 定位jre
  2. 解析命令行,比如傳入-XX:SDKGJ導致虛擬機退出就是在這里發生的。
  3. 加載jvm.dll,獲取JNI_CreateJavaVM等函數地址

第三點非常重要,它是啟動器調用hotspot JNI的橋梁。說着這么誇張,其實做起來是非常簡單的,就是LoadLibrary()加載jvm.dll然后GetProcAddress()運行時獲取JNI_CreateJavaVM地址轉化為函數指針,對應linux的dlopen,dlsym。然后經過一些中轉,啟動器會走到JavaMain。

jaba.base -> JLI_Launcher -> JavaMain

JavaMain維護hotspot的一個生命周期,它溝通java啟動器與hotspot世界,完成java.exe的功能:

int JNICALL
JavaMain(void * _args)
{
	...

     /* 初始化虛擬機 */
    start = CounterGet();
    /* 這里初始化虛擬機調用的就是之前提到的在jvm.dll里面獲取到的JNI_CreateJavaVM函數指針
     * 可以說,這里的JNI_CreateJavaVM是hotspot世界最先出現的地方
     */
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }

    /* 對虛擬機啟動進行性能profiling */
    if (JLI_IsTraceLauncher()) {
        end = CounterGet();
        JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
               (long)(jint)Counter2Micros(end-start));
    }

    ret = 1;

    /* 加載main函數所在的類 */
    mainClass = LoadMainClass(env, mode, what);
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);

    /* 對GUI程序的支持 */
    appClass = GetApplicationClass(env);
    mainArgs = CreateApplicationArgs(env, argv, argc);
    if (dryRun) {
        ret = 0;
        LEAVE();
    }
    PostJVMInit(env, appClass, vm);

    /* 獲取main方法id */
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");

    /* main方法調用 */
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    /* 啟動器的返回值(非System.exit退出) */
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

    LEAVE();
}

JavaMain這個函數做了我們通常意義上所認為啟動器應該做的事情,它:

  • 初始化虛擬機,
  • 獲取main所在的類
  • 調用main方法
  • 處理返回值

到這里java啟動器流程基本上已經清晰了,但是旅程並未結束。除了java啟動器外,本文還想探究一下main方法的調用。

jaba.base -> JLI_Launcher -> JavaMain -> CallStaticVoidMethod

首先,歡迎來到hotspot的世界。前面說到main方法的調用是這么一行代碼:

(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

那么它是怎么進入hotspot的世界的呢,要回答這個問題得看看env這是個什么東西。
env類似於這樣一個結構:

struct P{
    void (*jni_f1)(int,int);
    void (*jni_f2)();
    void (*jni_f3)(double);
};
P* env;

然后(*env)->jni_f1(3,4)調用的就是這個jni_f1函數指針,這些指針指向的是hotspot/share/primes/jni.cpp里面的入口點。

jaba.base -> JLI_Launcher -> JavaMain -> CallStaticVoidMethod -> JavaCalls::call

回到上面的代碼,CallStaticVoidMethod函數指針指向的就是JNI里面的函數:

JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
  JNIWrapper("CallStaticVoidMethod");
  HOTSPOT_JNI_CALLSTATICVOIDMETHOD_ENTRY(env, cls, (uintptr_t) methodID);
  DT_VOID_RETURN_MARK(CallStaticVoidMethod);

  va_list args;
  va_start(args, methodID);
  JavaValue jvalue(T_VOID);
  JNI_ArgumentPusherVaArg ap(methodID, args);
  jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
  va_end(args);
JNI_END

static void jni_invoke_static(JNIEnv *env, JavaValue* result, jobject receiver, JNICallType call_type, jmethodID method_id, JNI_ArgumentPusher *args, TRAPS) {
  methodHandle method(THREAD, Method::resolve_jmethod_id(method_id));

  // 創建java調用的參數
  ResourceMark rm(THREAD);
  int number_of_parameters = method->size_of_parameters();
  JavaCallArguments java_args(number_of_parameters);
  args->set_java_argument_object(&java_args);

  assert(method->is_static(), "method should be static");

  args->iterate( Fingerprinter(method).fingerprint() );

  // 初始化返回值類型
  result->set_type(args->get_ret_type());

  // main方法調用
  JavaCalls::call(result, method, &java_args, CHECK);

  // 返回值轉換
  if (result->get_type() == T_OBJECT || result->get_type() == T_ARRAY) {
    result->set_jobject(JNIHandles::make_local(env, (oop) result->get_jobject()));
  }
}

jni_CallStaticVoidMethod只是處理了一下可變參數,其他工作交給jni_invoke_static。這個函數會把之前傳入的命令行參數轉換為虛擬機里面的oop對象,然后最終通過JavaCalls::call調用了main函數。這不是個特例,java的所有方法調用都是通過JavaCalls::call調用的,它會創建解釋執行所需的棧幀,然后識別hotspot的模板解釋器入口點,進入這個入口點執行字節碼,當然這都是后話,如果有的話...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM