1. 程式人生 > >Jvm(jdk8)原始碼分析1-java命令啟動流程詳解

Jvm(jdk8)原始碼分析1-java命令啟動流程詳解

1.概述

現在大多數網際網路公司都是使用java技術體系搭建自己的系統,所以對java開發工程師以及java系統架構師的需求非常的多,雖然普遍的要求都是需要熟悉各種java開發框架(如目前比較流行ssi或者ssh框架),但是對於java語言本身的理解才是本質。如果你熟悉jvm原理以及jdk本身的實現,我相信對於其他開發框架的學習和深入理解應該不是很困難,因為很多靈活和高大山的框架都使用了jdk最核心的功能。除了本身框架的使用之外,凡是使用java語言開發的系統都避免不了對jvm的調優(對於系統性能要求不高可能不需要,但是對於網際網路公司來說效能好像是對系統的基本要求)。如果能夠深入掌握jvm原理,對於調優

jvm和解決各種java相關問題是很有幫助的,當然寫的java程式碼自然質量是很高的。

雖然我以前使用java進行編碼的時間很少,對很多java的高階功能也不是很熟悉,對於jvm原理和調優也是一知半解,但是這不影響我對jvm本身原理及程式碼實現的學習和研究。以前研究和學習linux的原始碼就覺得其樂無窮,相信現在研究jvm的原始碼應該也有同樣的感受,並且將有非常大的收穫。

正好現在java 8已經推出,業界對java8也是比較滿意。作為自己學習和研究完全就可以從java8開始了,直接通過hg工具(類似git)下載jdk8的原始碼進行研究學習:hg clone http://hg.openjdk.java.net/jdk8/jdk8

。下載原始碼以後就可以開始編譯了,具體請檢視幫助文件吧。編譯完成以後就可以執行java或者javac等相關命令了。

2.Java啟動

在學習原始碼的時候,首先需要找到程式入口函式main,但是由於原始碼太龐大而且可能有多個main函式,那麼怎麼可以快速的找到真正的入口main函式呢?這裡在linux就可以藉助除錯工具gdb了。例如我們要快速找到java的啟動入口函式,首先執行下面的命令gdb ./java會出現如下的資訊:

GNU gdb (Ubuntu 7.8-1ubuntu4) 7.8.0.20141001-cvs

Copyright (C) 2014 Free Software Foundation, Inc.

License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>

This is free software: you are free to change and redistribute it.

There is NO WARRANTY, to the extent permitted by law.  Type "show copying"

and "show warranty" for details.

This GDB was configured as "x86_64-linux-gnu".

Type "show configuration" for configuration details.

For bug reporting instructions, please see:

<http://www.gnu.org/software/gdb/bugs/>.

Find the GDB manual and other documentation resources online at:

<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".

Type "apropos word" to search for commands related to "word"...

Reading symbols from ./java...done.

(gdb) 

然後就進入了gdb的命令行了,這個時候使用l命令就可以看到啟動檔案的程式碼了,如下:

(gdb) l

80 char **__initenv;

81

82 int WINAPI

83 WinMain(HINSTANCE inst, HINSTANCE previnst, LPSTR cmdline, int cmdshow)

84 {

85     int margc;

86     char** margv;

87     const jboolean const_javaw = JNI_TRUE;

88

89     __initenv = _environ;

但是之看到這個啟動檔案中的開始程式碼,它不是第一行執行的程式碼,而且現在也不知道具體那個檔案。不過我還是可以利用斷點功能,我們都知道c語言的入口都是main函式,所以我們只需要對main進行打斷點即可,相關命令和輸出如下:

(gdb) b main

Breakpoint 1 at 0x4005f0: file /home/brucewoo/hg/jdk8/jdk/src/share/bin/main.c, line 94.

怎麼樣?現在足夠明顯了嗎?其他程式可以採用同樣的方式獲得程式的入口函式在哪一個檔案的哪一行。我們開啟這個檔案驗證一下確實是。那我們就一起看看這個入口程式碼,如下:

#ifdef JAVAW

省略的windows平臺相關的程式碼

#else /* JAVAW */

int main(int argc, char **argv)

{

    int margc;

    char** margv;

    const jboolean const_javaw = JNI_FALSE;

#endif /* JAVAW */

#ifdef _WIN32

    省略的windows平臺相關的程式碼

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

}

然後繼續看函式JLI_Launch,它接著進行java的啟動。程式碼如下:

static jlong threadStackSize    = 0;  /* stack size of the new thread */

static jlong maxHeapSize        = 0;  /* max heap size */

static jlong initialHeapSize    = 0;  /* inital heap size */

int JLI_Launch(int argc, char ** argv,          /* main argcargc */

        int jargc, const char** jargv,          /* java args */

        int appclassc, const char** appclassv,  /* appclasspath */

        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,                    /* classpathwildcard*/

        jboolean javaw,                         /* windows-only javaw */

        jint ergo                               /* ergonomics class policy */

)

{

    int mode = LM_UNKNOWN;

    char *what = NULL;

    char *cpath = 0;

    char *main_class = NULL;

    int ret;

    InvocationFunctions ifn;//函式指標的集合

    jlong startend;

    char jvmpath[MAXPATHLEN];//jvm的路徑

    char jrepath[MAXPATHLEN];//jre的路徑

    char jvmcfg[MAXPATHLEN]; //jvm配置路徑

    _fVersion = fullversion;

    _dVersion = dotversion;

    _launcher_name = lname;

    _program_name = pname;

    _is_java_args = javaargs;

    _wc_enabled = cpwildcard;

    _ergo_policy = ergo;

//Initialize platform specific settings,

//會根據_JAVA_LAUNCHER_DEBUG環境變數是否設定來設定是否列印debug資訊

    InitLauncher(javaw);

    DumpState();//根據是否設定debug來選擇輸出一些配置資訊

    if (JLI_IsTraceLauncher()) {//同樣如果設定了debug資訊就輸出命令列引數的輸出

        int i;

        printf("Command line args:\n");

        for (i = 0; i < argc ; i++) {

            printf("argv[%d] = %s\n", i, argv[i]);

        }

        AddOption("-Dsun.java.launcher.diag=true", NULL);

    }

    /*

     * Make sure the specified version of the JRE is running.

     *

     * There are three things to note about the SelectVersion() routine:

     *  1) If the version running isn't correct, this routine doesn't

     *     return (either the correct version has been exec'd or an error

     *     was issued).

     *  2) Argc and Argv in this scope are *not* altered by this routine.

     *     It is the responsibility of subsequent code to ignore the

     *     arguments handled by this routine.

     *  3) As a side-effect, the variable "main_class" is guaranteed to

     *     be set (if it should ever be set).  This isn't exactly the

     *     poster child for structured programming, but it is a small

     *     price to pay for not processing a jar file operand twice.

     *     (Note: This side effect has been disabled.  See comment on

     *     bugid 5030265 below.)

     */

    SelectVersion(argc, argv, &main_class);//選擇執行時jre的版本,規則看上面註釋

//建立執行的環境變數

    CreateExecutionEnvironment(&argc, &argv, jrepath, sizeof(jrepath),

                               jvmpath, sizeof(jvmpath), jvmcfg,  sizeof(jvmcfg));

    ifn.CreateJavaVM = 0;

    ifn.GetDefaultJavaVMInitArgs = 0;

    if (JLI_IsTraceLauncher()) {

        start = CounterGet();

    }

    if (!LoadJavaVM(jvmpath, &ifn)) {

        return(6);

    }

    if (JLI_IsTraceLauncher()) {

        end   = CounterGet();

    }

    JLI_TraceLauncher("%ldmicro seconds to LoadJavaVM\n",

             (long)(jint)Counter2Micros(end-start));

    ++argv;

    --argc;

    if (IsJavaArgs()) {

        /* Preprocess wrapper arguments */

        TranslateApplicationArgs(jargc, jargv, &argc, &argv);

        if (!AddApplicationOptions(appclassc, appclassv)) {

            return(1);

        }

    } else {

        /* Set default CLASSPATH */

        cpath = getenv("CLASSPATH");

        if (cpath == NULL) {

            cpath = ".";

        }

        SetClassPath(cpath);

    }

    /* Parse command line options; if the return value of

     * ParseArguments is false, the program should exit.

     */

    if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))

    {

        return(ret);

    }

    /* Override class path if -jar flag was specified */

    if (mode == LM_JAR) {

        SetClassPath(what);     /* Override class path */

    }

    /* set the -Dsun.java.command pseudo property */

    SetJavaCommandLineProp(what, argc, argv);

    /* Set the -Dsun.java.launcher pseudo property */

    SetJavaLauncherProp();

    /* set the -Dsun.java.launcher.* platform properties */

    SetJavaLauncherPlatformProps();

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

}

接下來詳細分析這個主流程中的各個重要函式。

(1)SelectVersion:選擇jre的版本,這個函式實現的功能比較簡單,就是選擇正確的jre版本來作為即將執行java程式的版本。選擇的方式,如果環境變數設定了_JAVA_VERSION_SET,那麼代表已經選擇了jre的版本,不再進行選擇;否則,根據執行時給定的引數來搜尋不同的目錄選擇,例如指定版本和限制了搜尋目錄等,也可能執行的是一個jar檔案,所以需要解析manifest檔案來獲取相關資訊,對應Manifest檔案的資料結構,通過函式ParseManifest解析,具體請看下面註釋。

/*

 * Information returned from the Manifest file by the ParseManifest() routine.

 * Certainly (much) more could be returned, but this is the information

 * currently of interest to the C based Java utilities (particularly the

 * Java launcher).

 */

typedef struct manifest_info {  /* Interesting fields from the Manifest */

    char        *manifest_version;      /* Manifest-Version string */

    char        *main_class;            /* Main-Class entry */

    char        *jre_version;           /* Appropriate J2SE release spec */

    char        jre_restrict_search;    /* Restricted JRE search */

    char        *splashscreen_image_file_name/* splashscreen image file */

manifest_info;

最終會解析出一個真正需要的jre版本並且判斷當前執行本java程式的jre版本是不是和這個版本一樣,如果不一樣呼叫linux的execv函式終止當前進出並且使用新的jre版本重新執行這個java程式,但是程序ID不會改變。

(2)CreateExecutionEnvironment,這個函式主要建立執行的一些環境,這個環境主要是指jvm的環境,例如需要確定資料模型,是32位還是64位以及jvm本身的一些配置在jvm.cfg檔案中讀取和解析。裡面有一個重要的函式就是專門解析jvm.cfg的,如下:jint ReadKnownVMs(const char *jvmCfgName, jboolean speculative)。這個函式解析jvm.cfg檔案來確定jvm的型別,jvm的型別有如下幾種(是一個列舉定義):

/* Values for vmdesc.flag */

enum vmdesc_flag {

    VM_UNKNOWN = -1,

    VM_KNOWN,

    VM_ALIASED_TO,

    VM_WARN,

    VM_ERROR,

    VM_IF_SERVER_CLASS,

    VM_IGNORE

};

然後還有一個結構體專門描述jvm的資訊,如下:

struct vmdesc {

    char *name;//名字

    int flag;//上面的列舉定義型別

    char *alias;//別名

    char *server_class;//伺服器類

};

總結:這個函式主要就是確定一下jvm的資訊並且初始化相關資訊,為後面的jvm執行準備環境。

(3)LoadJavaVM:動態載入jvm.so這個共享庫,並把jvm.so中的相關函式匯出並且初始化,例如JNI_CreateJavaVM函式。後期啟動真正的java虛擬就是通過這裡面載入的函式,裡面重要的程式碼如下:

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

總結:這個函式就是初始化jvm相關的初始化函式和入後函式,後面就是呼叫這裡的JNI_CreateJavaVM函式真正的開始啟動一個jvm的,這個函式會做很多的初始化工作,基本上一個完整的jvm資訊在這個函式裡面都能夠看到,後面單獨詳細講解這個函式。

(4)ParseArguments:解析命令列引數,就不多解析了,不同的命令列引數具體使用到來詳細介紹其作用。

(5)JVMInit:這是啟動流程最後執行的一個函式,如果這個函式返回了那麼這個java啟動就結束了,所有這個函式最終會以某種形式進行執行下去。具體先看看這個函式的主要流程,如下:

JVMInit->ContinueInNewThread->ContinueInNewThread0->(可能是新執行緒的入口函式進行執行,新執行緒建立失敗就在原來的執行緒繼續支援這個函式)JavaMain->InitializeJVM(初始化jvm,這個函式呼叫jvm.so裡面匯出的CreateJavaVM函式建立jvm了,JNI_CreateJavaVM這個函式很複雜)->LoadMainClass(這個函式就是找到我們真正java程式的入口類,就是我們開發應用程式帶有main函式的類)->GetApplicationClass->後面就是呼叫環境類的工具獲得main函式並且傳遞引數呼叫main函式,查詢main和呼叫main函式都是使用類似java裡面支援的反射實現的。

到此java這個啟動命令全部流程解析完畢,但是其中還有很重要的兩個流程沒有分析。一個就是初始化和啟動真正的jvm,由動態連結庫jvm.so中的JNI_CreateJavaVM實現,另外一個就是最後查詢入口類以及查詢main入口函式的具體實現。這兩個都涉及到很多的內容,後面會分別單獨一篇文章來分析。