JDK8加載源碼分析
現在大多數互聯網公司都是使用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 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 */
)
{
int mode = LM_UNKNOWN;
char *what = NULL;
char *cpath = 0;
char *main_class = NULL;
int ret;
InvocationFunctions ifn;//函數指針的集合
jlong start, end;
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("%ld micro 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入口函數的具體實現。這兩個都涉及到很多的內容,后面會分別單獨一篇文章來分析。
【源】:https://blog.csdn.net/qiangweiloveforever/article/details/51810294