Java虛擬機的啓動與程序的運行

這篇文章是從 OpenJDK 源代碼的角度講當咱們運行了java

java -classpath . hello

以後,java.exe 如何從 main 函數開始執行,啓動虛擬機,並執行字節碼中的代碼。linux

實驗環境

要了解一個系統是如何運行的,光看是不行的,要實際地運行,調試,修改才能對系統的動做方式有所瞭解。git

起初我是按照 GitHub 上的一個項目 OpenJDK-Research 在 windows 7 64位平臺上,使用 Visual Studio 2010 來調試,運行的。可是後來發現,這個項目僅僅編譯了HotSpot虛擬機, java.exe 並無編譯。github

這裏咱們首先弄明白 java.exe 和虛擬機之間的關係。咱們使用 Visual Studio 編譯出的 HotSpot 是虛擬機,是做爲動態連接庫的形式被 java.exe 加載的。java.exe 負責解析參數,加載虛擬機連接庫,它須要調用虛擬機中的函數來完成執行 Java 程序的功能。因此,你在HotSpot的源代碼中找不到啓動的程序的 main 函數,原本在 openjdk7 中,虛擬機是帶有一個啓動器的,在目錄 openjdk/hotspot/src/share/tools/launcher/java.c 中能夠找到 main 函數,可是在 openjdk8 中,這個啓動器不見了,被放在 openjdk/jdk 目錄下,而不是 openjdk/hotspot 目錄下了,給咱們的學習過程形成了傷害。windows

因此我後來就在 linux 平臺上調試了,由於在 windows 平臺上,我始終沒有把整個 openjdk8 編譯成功,編譯不出java.exe, 僅僅編譯了 hotspot,是看不到從 main 函數開始的執行的。關於如何在 linux 平臺下編譯調試 openjdk8,能夠參考個人另外一篇文章 在Ubuntu 12.04 上編譯 openjdk8.數據結構

調用棧

jdk8u/jdk/src/share/bin/main.c::WinMain/main
  jdk8u/jdk/src/share/bin/java.c::JLI_Launch
    jdk8u/jdk/src/solaris/bin/java_md_solinux.c::LoadJavaVM # Load JVM Library: libjvm.so
    jdk8u/jdk/src/solaris/bin/java_md_solinux.c::JVMInit # Create JVM
      jdk8u/jdk/src/share/bin/java.c::ContinueInNewThread
        jdk8u/jdk/src/solaris/bin/java_md_solinux.c::ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
          pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args)
            jdk8u/jdk/src/share/bin/java.c::JavaMain
              jdk8u/jdk/src/share/bin/java.c::InitializeJVM
                jdk8u\hotspot\src\share\vm\prims\jni.cpp::JNI_CreateJavaVM

執行過程

  • main.c (jdk8u/jdk/src/share/bin/main.c)
#ifdef JAVAW

char **__initenv;

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

    __initenv = _environ;
#else /* JAVAW */
int
main(int argc, char **argv)
{
    int margc;
    char** margv;
    const jboolean const_javaw = JNI_FALSE;
#endif /* JAVAW */
#ifdef _WIN32
    {
        int i = 0;
        if (getenv(JLDEBUG_ENV_ENTRY) != NULL) {
            printf("Windows original main args:\n");
            for (i = 0 ; i < __argc ; i++) {
                printf("wwwd_args[%d] = %s\n", i, __argv[i]);
            }
        }
    }
    JLI_CmdToArgs(GetCommandLine());
    margc = JLI_GetStdArgc();
    // add one more to mark the end
    margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
    {
        int i = 0;
        StdArg *stdargs = JLI_GetStdArgs();
        for (i = 0 ; i < margc ; i++) {
            margv[i] = stdargs[i].arg;
        }
        margv[i] = NULL;
    }
#else /* *NIXES */
    margc = argc;
    margv = argv;
#endif /* WIN32 */
    return JLI_Launch(margc, margv,
                   sizeof(const_jargs) / sizeof(char *), const_jargs,
                   sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
                   FULL_VERSION,
                   DOT_VERSION,
                   (const_progname != NULL) ? const_progname : *margv,
                   (const_launcher != NULL) ? const_launcher : *margv,
                   (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
                   const_cpwildcard, const_javaw, const_ergo_class);
}

這就是傳說中的 main 函數的真身,能夠看出,它針對操做系統是否使用 Windows ,執行了不一樣的代碼段,最終調用JLI_Launch 函數。app

  • JLI_Lanuch(jdk8u/jdk/src/share/bin/java.c)
int
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                               /* ergonomics class policy */
)
{

...

    if (!LoadJavaVM(jvmpath, &ifn)) {
        return(6);
    }

...

    return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);

}

從這裏能夠看出 JLI_Lanuch 的各個參數的含義, 我列出了關鍵代碼, 其中 LoadJavaVM 完成載入虛擬機動態連接庫,並初始化 ifn 中的函數指針,HotSpot虛擬機就是這樣向啓動器 java 提供功能。jvm

  • LoadJavaVM (jdk8u/jdk/src/solaris/bin/java_md_solinux.c)

這個函數涉及動態連接庫,不一樣操做系統有不一樣接口,這裏是針對 linux 的。函數

jboolean
LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn)
{
    ...

    libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);

    ...

    ifn->CreateJavaVM = (CreateJavaVM_t)
    dlsym(libjvm, "JNI_CreateJavaVM");

    ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)
    dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");

    ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)
        dlsym(libjvm, "JNI_GetCreatedJavaVMs");

  ...

從這裏能夠看出載入動態連接庫以及初始化 ifn 數據結構的代碼。在個人調試版本中,javapath 指向以前編譯出的動態連接庫 jdk8u/build/fastdebug/jdk/lib/i386/server/libjvm.so.post

  • JVM_Init(jdk8u/jdk/src/solaris/bin/java_md_solinux.c)

回到 JLI_Lanuch 函數,咱們最終進入 JVM_Init, 這個函數會啓動一個新線程。

int
JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
        int argc, char **argv,
        int mode, char *what, int ret)
{
    ShowSplashScreen();
    return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
}

ContinueInNewThread 會調用另外一個函數 ContinueInNewThread0 啓動線程,執行 JavaMain 函數:

int
ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {

...

    if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
      void * tmp;
      pthread_join(tid, &tmp);
      rslt = (int)tmp;
    } else {
     /*
      * Continue execution in current thread if for some reason (e.g. out of
      * memory/LWP)  a new thread can't be created. This will likely fail
      * later in continuation as JNI_CreateJavaVM needs to create quite a
      * few new threads, anyway, just give it a try..
      */
      rslt = continuation(args);
    }

...


  • JavaMain(jdk8u/jdk/src/share/bin/java.c)

這個函數會初始化虛擬機,加載各類類,並執行應用程序中的 main 函數。註釋很詳細。

int JNICALL
JavaMain(void * _args)
{
    JavaMainArgs *args = (JavaMainArgs *)_args;
    int argc = args->argc;
    char **argv = args->argv;
    int mode = args->mode;
    char *what = args->what;
    InvocationFunctions ifn = args->ifn;

    JavaVM *vm = 0;
    JNIEnv *env = 0;
    jclass mainClass = NULL;
    jclass appClass = NULL; // actual application class being launched
    jmethodID mainID;
    jobjectArray mainArgs;
    int ret = 0;
    jlong start, end;

    RegisterThread();

    /* Initialize the virtual machine */
    start = CounterGet();
    if (!InitializeJVM(&vm, &env, &ifn)) {
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }

    ...

    ret = 1;

    /*
     * Get the application's main class.
     *
     * See bugid 5030265.  The Main-Class name has already been parsed
     * from the manifest, but not parsed properly for UTF-8 support.
     * Hence the code here ignores the value previously extracted and
     * uses the pre-existing code to reextract the value.  This is
     * possibly an end of release cycle expedient.  However, it has
     * also been discovered that passing some character sets through
     * the environment has "strange" behavior on some variants of
     * Windows.  Hence, maybe the manifest parsing code local to the
     * launcher should never be enhanced.
     *
     * Hence, future work should either:
     *     1)   Correct the local parsing code and verify that the
     *          Main-Class attribute gets properly passed through
     *          all environments,
     *     2)   Remove the vestages of maintaining main_class through
     *          the environment (and remove these comments).
     *
     * This method also correctly handles launching existing JavaFX
     * applications that may or may not have a Main-Class manifest entry.
     */
    mainClass = LoadMainClass(env, mode, what);
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);
    /*
     * In some cases when launching an application that needs a helper, e.g., a
     * JavaFX application with no main method, the mainClass will not be the
     * applications own main class but rather a helper class. To keep things
     * consistent in the UI we need to track and report the application main class.
     */
    appClass = GetApplicationClass(env);
    NULL_CHECK_RETURN_VALUE(appClass, -1);
    /*
     * PostJVMInit uses the class name as the application name for GUI purposes,
     * for example, on OSX this sets the application name in the menu bar for
     * both SWT and JavaFX. So we'll pass the actual application class here
     * instead of mainClass as that may be a launcher or helper class instead
     * of the application class.
     */
    PostJVMInit(env, appClass, vm);
    /*
     * The LoadMainClass not only loads the main class, it will also ensure
     * that the main method's signature is correct, therefore further checking
     * is not required. The main method is invoked here so that extraneous java
     * stacks are not in the application stack trace.
     */
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");
    CHECK_EXCEPTION_NULL_LEAVE(mainID);

    /* Build platform specific argument array */
    mainArgs = CreateApplicationArgs(env, argv, argc);
    CHECK_EXCEPTION_NULL_LEAVE(mainArgs);

    /* Invoke main method. */
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    /*
     * The launcher's exit code (in the absence of calls to
     * System.exit) will be non-zero if main threw an exception.
     */
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
    LEAVE();
}

注意 InitializeJVM 函數,它會調用以前初始化的 ifn 數據結構中的 CreateJavaVM 函數.

  • InitializeJVM(jdk8u/jdk/src/share/bin/java.c::InitializeJVM)
static jboolean
InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
{
    JavaVMInitArgs args;
    jint r;

    memset(&args, 0, sizeof(args));
    args.version  = JNI_VERSION_1_2;
    args.nOptions = numOptions;
    args.options  = options;
    args.ignoreUnrecognized = JNI_FALSE;

    if (JLI_IsTraceLauncher()) {
        int i = 0;
        printf("JavaVM args:\n    ");
        printf("version 0x%08lx, ", (long)args.version);
        printf("ignoreUnrecognized is %s, ",
               args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
        printf("nOptions is %ld\n", (long)args.nOptions);
        for (i = 0; i < numOptions; i++)
            printf("    option[%2d] = '%s'\n",
                   i, args.options[i].optionString);
    }

    r = ifn->CreateJavaVM(pvm, (void **)penv, &args);
    JLI_MemFree(options);
    return r == JNI_OK;
}

ifn->CreateJavaVM指向虛擬機動態連接庫中的 JNI_CreateJavaVM 函數,這個函數會真正建立虛擬機。 這個函數執行後,pvm, penv 的值就會被設定,咱們能夠比較下執行先後它們的值,來看看它們的做用。

// before r = ifn->CreateJavaVM(pvm, (void **)penv, &args);

(gdb) p *pvm
$8 = (JavaVM *) 0x0
(gdb) p *penv
$9 = (JNIEnv *) 0x0
// after r = ifn->CreateJavaVM(pvm, (void **)penv, &args);

(gdb) p ***penv
$14 = {reserved0 = 0x0, reserved1 = 0x0, reserved2 = 0x0, reserved3 = 0x0, 
  GetVersion = 0xb6ede599 <jni_GetVersion>, 
  DefineClass = 0xb6eb20a0 <jni_DefineClass>, 
  FindClass = 0xb6eb253c <jni_FindClass>, 
  FromReflectedMethod = 0xb6eb2b17 <jni_FromReflectedMethod>, 
  FromReflectedField = 0xb6eb2edb <jni_FromReflectedField>, 
  ...
  ...
  }

(gdb) p ***pvm
$15 = {reserved0 = 0x0, reserved1 = 0x0, reserved2 = 0x0, 
  DestroyJavaVM = 0xb6edf1e8 <jni_DestroyJavaVM>, 
  AttachCurrentThread = 0xb6edf69a <jni_AttachCurrentThread>, 
  DetachCurrentThread = 0xb6edf795 <jni_DetachCurrentThread>, 
  GetEnv = 0xb6edf8d3 <jni_GetEnv>, 
  AttachCurrentThreadAsDaemon = 0xb6edfa7d <jni_AttachCurrentThreadAsDaemon>}

能夠看出它們獲得了hotspot 中以 jni_ 開頭的一些函數,虛擬機正是以這樣的方式向外提供功能。咱們大概看一下JNI_CreateJavaVM 的功能。


  • JNI_CreateJavaVM(jdk8u\hotspot\src\share\vm\prims\jni.cpp)
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
  ...

    result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);
  if (result == JNI_OK) {
    JavaThread *thread = JavaThread::current();
    /* thread is thread_in_vm here */
    *vm = (JavaVM *)(&main_vm);
    *(JNIEnv**)penv = thread->jni_environment();

    // Tracks the time application was running before GC
    RuntimeService::record_application_start();

    // Notify JVMTI
    if (JvmtiExport::should_post_thread_life()) {
       JvmtiExport::post_thread_start(thread);
    }

    ...

  }

  ...

}

其中的 create_vm 函數是虛擬機初始化的關鍵,它初始化了虛擬機的大部分組件。另外能夠看到 vm, penv 的值被設定。

這個函數位於 jdk8u\hotspot\src\share\vm\prims\jni.cpp

我以前在 Windows 下調試,直接調試的 HotSpot 動態連接庫,能夠看到的第一個函數就是 JNI_CreateJavaVM, 以前的調用都位於 java.exe 代碼中。由於 Windows 中 java.exe 不是咱們本身編譯的,看不到其中調用關係。以下圖所示:


同時能夠看到兩個線程

相關文章
相關標籤/搜索