Java虚拟机启动整体流程和基础学习(内容很多,不可快餐阅读),推理+源码论证
不啰嗦,直接看:
ISA指令強關聯CPU平臺,那么匯編語言也一定強關聯CPU平臺,而C語言又是對匯編最簡單的抽象,也就一定會耦合不同CPU平臺的代碼,這也就是意味著,在寫C/C++項目的時候,必須要考慮不同平臺的代碼編寫差異,要根據不同的平臺寫不同的代碼以滿足不同平臺的功能使用與相關優化。
而我們知道程序想要操作硬件就必須要經過操作系統,由操作系統給定的系統調用去進行硬件上的操作
OS內核是在進程的虛擬地址的1GB空間,作為進程的內核態來執行,也就是可以理解為一串代碼,不是獨立的執行的軟件
也就是說,如果想要一個App能在不同的操作系統上運行,那么必須要有不同的操作系統宏處理或者文件以適配不同系統的系統調用
除了通過OS的系統調用操作計算機硬件外,還可以通過內聯匯編直接操作CPU
對于C/C++開發人員來講,會有一個很重要的問題需要去考慮:指針問題
野指針(指向不明空間/無效內存),內存泄漏(忘記釋放),內存溢出等問題
為了規避這種操作指針操作帶來的問題,那么就有兩種解決方案:
讓編譯器識別指針問題,但是這個方式也帶來不足之處,由于如果出現任何指針問題則編譯不通過,使得語言編寫相對死板,就不能使用一些特殊的寫法讓代碼量減少
自動釋放指針,也即垃圾回收機制。基于此想法,便出現了Java
那么Java編譯器該怎么設計呢?
直接生成 ISA指令集?是可以的,但是Java語言開發者是想要適配多個不同的CPU平臺,適配不同OS,如果直接生成ISA,那豈不是和其他的靜態語言一樣了?
那么是將Java編譯生成抽象語法樹,然后用C/C++去識別和處理這個抽象語法樹?亦或者是將Java編譯生成一個中間語言(或稱中間表示)然后交由c/c++去處理呢?
很顯然,Java選擇了后者,為什么呢??動態性,如果選擇抽象語法樹,那么所有的動態性全都需要在c++引擎層面去實現,而使用中間語言則可以讓Java語言與C++脫鉤,只需要C++識別處理中間語言即可
這個中間語言便是熟知的 字節碼
最終便得到了:
用C++代碼解釋字節碼文件,但是這樣就會出現一個問題,c++讀取字節碼后遍歷字節碼,然后條件判斷再進行不同的處理
但是這樣性能不高,即便C++再怎么優化也不如直接生成匯編語言來的快,甚至可以預先將匯編語言生成好機器碼,然后直接拿機器碼去運行
因此,就有了如下幾種機制:
C++解釋字節碼
字節碼直接映射成機器碼執行,由機器碼解釋
動態編譯優化(JIT)
運行速度:
C++解釋 < 機器碼解釋 < 編譯優化
啟動速度:
C++解釋 > 機器碼解釋 > 編譯優化
根據熱點的方法進行選擇性編譯,也就是:
1、按照解釋執行啟動方法
2、根據調用的熱點(Hotspot)方法 運行 JIT 動態編譯優化
GCC存在 -O1、- O2 、-O3等優化等級,每一級的優化速度不同,性能也不同,那么在JIT中引入此方式,得到分層編譯,既滿足編譯速度也執行的速度
C++在不同的操作系統上的處理不同,因此圖變如下:
使得Java程序無需關心底層不同OS平臺是如何處理的,只需要專注于業務代碼的編寫即可
也即:Write Once,Run Everywhere,跨平臺
所以Java只需要面向字節碼編程即可
那既然要解釋字節碼,若不同虛擬機實現字節碼的方式不同,就會回到C/C++不同平臺不同處理,那么是不是需要有一定的規范如何去解釋?
《Java虛擬機規范》約束不同虛擬機廠商對于該字節碼的約定與解釋
由于字節碼底層操作的對象需要處理,那么因此引入了垃圾回收機制(GC模塊)
無需JAVA程序員做GC的工作,在GC模塊中實現自動垃圾回收
Java 字節碼解析:
public class Hello{public static void main(String[] args){System.out.println("Hello World");}}我們都知道,在C語言的程序中,函數傳參是放在棧幀或者寄存器中,字符串存放在.data下的.string 靜態數據域中
那我們可以猜測一下,Java的字節碼會不會和C語言的存放格式差不多呢?
使用javac命令編譯Hello.java得到 Hello.class字節碼文件,使用javap -v 命令查看字節碼信息
Classfile /D:/桌面/Hello.classLast modified 2022-9-29; size 415 bytesMD5 checksum ba0701759adea1ab8a220d2cded9f997Compiled from "Hello.java" public class Hellominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #6.#15 // java/lang/Object."<init>":()V#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #18 // Hello world#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #21 // Hello#6 = Class #22 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 main#12 = Utf8 ([Ljava/lang/String;)V#13 = Utf8 SourceFile#14 = Utf8 Hello.java#15 = NameAndType #7:#8 // "<init>":()V#16 = Class #23 // java/lang/System#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;#18 = Utf8 Hello world#19 = Class #26 // java/io/PrintStream#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V#21 = Utf8 Hello#22 = Utf8 java/lang/Object#23 = Utf8 java/lang/System#24 = Utf8 out#25 = Utf8 Ljava/io/PrintStream;#26 = Utf8 java/io/PrintStream#27 = Utf8 println#28 = Utf8 (Ljava/lang/String;)V {public Hello();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 5: 0line 7: 8 } SourceFile: "Hello.java"這樣一比較,和ELF文件展示方式一樣啊
那么我們需要解析這個字節碼文件,如何解析呢,我得知道排布格式吧?前面說到了《java虛擬機規范》定義的啊
規范定義什么,那底層實現就是什么,與操作系統和CPU平臺無關
也就是我可以通過讀入文件數據流,按照以上格式進行解析,即可得到字節碼文件中的相關信息
那按照這個虛擬機規范,hotspot是怎么解析這個classfile的呢?
猜測一下,是遍歷整個文件,然后按照對應的字節長度進行判斷解析
Java 加載,鏈接,初始化
Java沒有靜態鏈接,只有動態鏈接
而在C語言中,.so文件的GOT表和PLT表進行跳轉鏈接,那Java中的動態鏈接同樣需要找表
在字節碼中的constantPool就是ELF文件中的DynamicSymbol
如何做的呢?
讀入代碼段中的數據,找到invokespecial 對應的 constantPool中數據對應的地址解析出來,再把地址填進去執行即可
由于我們知道,需要按照一定的格式來讀取,那么我們需要考慮將這些數據保存起來
也就是需要有個容器來存放ClassFile Structure,猜測應該是放在與ClassFile相關的c++代碼中
而在Hotspot中沒有直接的classfile.cpp,而與之相關的只有classFileParser.cpp、classFileStream.cpp前者意思是字節碼解析器,那么肯定與字節碼處理相關的部分的代碼, 而后者是字節碼文件流,肯定是用來幫助讀取字節碼數據流的,因此我們得看classFileParser.cpp
在其中的parseClassFile方法中
通過查找其函數體,我們可以找到如下代碼:
分配實例類空間
也就是生成了一個InstanceKlass
Klass的描述為:
Klass的規定
在一個C++類中存在這兩個行為
所以說明,我們之前要找的字節碼解析完后就存放在這個地方
而我們知道對象有兩個部分,一個是屬性(field),一個是方法(method)
那ClassFileParser肯定是有這兩個部分的處理邏輯的,而且按照面向對象的思想,這兩個肯定也會分別存儲在某個容器中
看到這里我們分別要去看看parse_fields的產物和parse_methods的產物
根據上圖,字節碼屬性的信息存放在了FieldInfo中
按照手冊規定
parse_methods方法中的產物如下:
也就是將方法數據存放在了Method中:
那么除了方法和屬性之外,還得需要有個存放靜態數據的地方吧?也即常量池
解析常量池的方法是
最終可以得到如下圖:
對于單獨的一個類來講,在Java中是沒有什么太大作用的,那么就肯定是多個類,而多個類就肯定會相互關聯
上圖將會變化為下面這樣
那么類與類之間是怎么關聯在一起的呢??
在ELF中我們知道是有動態鏈接庫和靜態鏈接庫,然后通過PLT和GOT 拼接組裝生成可執行文件
是不是極度相似呢?
拿前面的字節碼來看
Classfile /D:/桌面/Hello.classLast modified 2022-9-29; size 415 bytesMD5 checksum ba0701759adea1ab8a220d2cded9f997Compiled from "Hello.java" public class Hellominor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #6.#15 // java/lang/Object."<init>":()V#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;#3 = String #18 // Hello world#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class #21 // Hello#6 = Class #22 // java/lang/Object#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 main#12 = Utf8 ([Ljava/lang/String;)V#13 = Utf8 SourceFile#14 = Utf8 Hello.java#15 = NameAndType #7:#8 // "<init>":()V#16 = Class #23 // java/lang/System#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;#18 = Utf8 Hello world#19 = Class #26 // java/io/PrintStream#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V#21 = Utf8 Hello#22 = Utf8 java/lang/Object#23 = Utf8 java/lang/System#24 = Utf8 out#25 = Utf8 Ljava/io/PrintStream;#26 = Utf8 java/io/PrintStream#27 = Utf8 println#28 = Utf8 (Ljava/lang/String;)V {public Hello();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 1: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc #3 // String Hello world5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 5: 0line 7: 8 } SourceFile: "Hello.java"例如,getstatic 后面的#2僅僅是個占位符,此時并沒有具體的地址數據,而在常量池表中#2 引用了#16和#17,同樣的,這里并沒有具體的地址數據值
那不就是和ELF中的動態加載嗎?先用一個無用的地址先占著,之后再通過GOT把真實的值填上
所以 ConstantPool就是等同于維護了ELF中的符號表(Dynamic_Symbol)的元數據信息
在Java源碼文件結構圖如下:
而我們可以很清楚的知道,作為一個程序員,是不是應該考慮將Java虛擬機源碼和操作Java虛擬機的工具代碼分開編譯??
所以就是拆分出來了 hotspot文件夾和jdk文件夾
根據elf文件來理解,hotspot是屬于Java虛擬機,工具可以共同操作的,因此hotspot虛擬機應該打包成.so/.dll動態鏈接庫文件
而jdk中的.c文件則編譯成可執行文件
Klass 類表示整個字節碼讀入內存中的容器信息
ConstantPool 類表示Java字節碼所操作的對象的符號信息
Method類 表示Java字節碼的方法描述信息
FieldInfo 表示Java的成員變量信息
而我們知道,在jvm中兩大板塊:執行引擎+GC
執行引擎如何加載Klass呢?因此需要用到類加載器
如此,得到下面這幅圖:
類加載器讀入字節碼文件,然后創建上面的Klass信息,傳遞給執行引擎,最后用GC進行垃圾回收
由于Java堆內存的管理是由universe進行管理
所以universe主要是兩個模塊:
在openJDK源碼包下的目錄層級如下:
其中:
common : 一些公共文件,比如下載源碼的shell腳本、生成make的autoconf等文件
corba : 分布式通訊接口
hotspot : Java虛擬機實現
jaxp : xml文件處理代碼
jaxws : ws實現api
jdk : 用于操作虛擬機的工具代碼,也即jdk源碼
langtools : java語言的工具實現的用于測試的代碼,基礎實現類等,javac,java,javap等 打包成 tools.jar
make : make編譯文件夾
nashorn : java中的js運行實現
test : 測試包
而我們研究的重點放在hotspot虛擬機的實現上以及jdk虛擬機操作工具的源碼上
也就是通過jdk包中的工具來引導執行引擎
如何引導?使用動態鏈接庫
而動態鏈接庫又是數據獨立,代碼共享的,所以使用工具命令可以調用到執行引擎的main
那么執行引擎的main方法是怎么樣的呢?由于我們只研究在linux平臺的,所以會屏蔽掉windows平臺的一些代碼,引導型的啟動代碼都是C語言代碼
main.c
int main(int argc, char **argv) {int margc;char** margv;const jboolean const_javaw = JNI_FALSE;margc = argc;margv = argv;// JLI_Launch()函數進行了一系列必要的操作,// 如libjvm.so的加載、參數解析、Classpath的 獲取和設置、系統屬性設置、JVM初始化等。// 調用LoadJavaVM()加載libjvm.so并初始化相關參數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); }我們需要找到動態鏈接庫的dlopen打開文件,dlsym查找符號,以及dlclose關閉 的相關處理
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 */ ) {int mode = LM_UNKNOWN;char *what = NULL;char *cpath = 0;char *main_class = NULL;int ret;InvocationFunctions ifn;jlong start, end;char jvmpath[MAXPATHLEN];char jrepath[MAXPATHLEN];char jvmcfg[MAXPATHLEN];_fVersion = fullversion;_dVersion = dotversion;_launcher_name = lname;_program_name = pname;_is_java_args = javaargs;_wc_enabled = cpwildcard;_ergo_policy = ergo;// 初始化 執行程序InitLauncher(javaw);// 導出當前執行狀態DumpState();// 如果JLI是追蹤執行程序if (JLI_IsTraceLauncher()) {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);// 創建執行環境CreateExecutionEnvironment(&argc, &argv,jrepath, sizeof(jrepath),jvmpath, sizeof(jvmpath),jvmcfg, sizeof(jvmcfg));ifn.CreateJavaVM = 0;ifn.GetDefaultJavaVMInitArgs = 0;if (JLI_IsTraceLauncher()) {start = CounterGet();}// 加載Java虛擬機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); }詳細去看看CreateExecutionEnvironment方法是如何創建執行環境的?
java_md_solinux.c
void CreateExecutionEnvironment(int *pargc, char ***pargv,char jrepath[], jint so_jrepath,char jvmpath[], jint so_jvmpath,char jvmcfg[], jint so_jvmcfg) {/** First, determine if we are running the desired data model. If we* are running the desired data model, all the error messages* associated with calling GetJREPath, ReadKnownVMs, etc. should be* output. However, if we are not running the desired data model,* some of the errors should be suppressed since it is more* informative to issue an error message based on whether or not the* os/processor combination has dual mode capabilities.*/jboolean jvmpathExists;/* Compute/set the name of the executable */SetExecname(*pargv);/* Check data model flags, and exec process, if needed */{char *arch = (char *)GetArch(); /* like sparc or sparcv9 */char * jvmtype = NULL;int argc = *pargc;char **argv = *pargv;int running = CURRENT_DATA_MODEL;int wanted = running; /* What data mode is beingasked for? Current model isfine unless another modelis asked for */jboolean mustsetenv = JNI_FALSE;char *runpath = NULL; /* existing effective LD_LIBRARY_PATH setting */char* new_runpath = NULL; /* desired new LD_LIBRARY_PATH string */char* newpath = NULL; /* path on new LD_LIBRARY_PATH */char* lastslash = NULL;char** newenvp = NULL; /* current environment */char** newargv = NULL;int newargc = 0;/** Starting in 1.5, all unix platforms accept the -d32 and -d64* options. On platforms where only one data-model is supported* (e.g. ia-64 Linux), using the flag for the other data model is* an error and will terminate the program.*/{ /* open new scope to declare local variables */int i;newargv = (char **)JLI_MemAlloc((argc+1) * sizeof(char*)); // 將傳入的值存放在一個新的變量中newargv[newargc++] = argv[0];/* scan for data model arguments and remove from argument list;last occurrence determines desired data model */for (i=1; i < argc; i++) {if (JLI_StrCmp(argv[i], "-J-d64") == 0 || JLI_StrCmp(argv[i], "-d64") == 0) {wanted = 64;continue;}if (JLI_StrCmp(argv[i], "-J-d32") == 0 || JLI_StrCmp(argv[i], "-d32") == 0) {wanted = 32;continue;}newargv[newargc++] = argv[i];if (IsJavaArgs()) {if (argv[i][0] != '-') continue;} else {if (JLI_StrCmp(argv[i], "-classpath") == 0 || JLI_StrCmp(argv[i], "-cp") == 0) { // 自動添加了classpathi++;if (i >= argc) break;newargv[newargc++] = argv[i];continue;}if (argv[i][0] != '-') { i++; break; }}}/* copy rest of args [i .. argc) */while (i < argc) {newargv[newargc++] = argv[i++];}newargv[newargc] = NULL;/** newargv has all proper arguments here*/argc = newargc;argv = newargv;}/* If the data model is not changing, it is an error if thejvmpath does not exist */if (wanted == running) {/* Find out where the JRE is that we will be using. */if (!GetJREPath(jrepath, so_jrepath, arch, JNI_FALSE) ) { // 找到jre的路徑JLI_ReportErrorMessage(JRE_ERROR1);exit(2);}JLI_Snprintf(jvmcfg, so_jvmcfg, "%s%slib%s%s%sjvm.cfg",jrepath, FILESEP, FILESEP, arch, FILESEP);/* Find the specified JVM type */if (ReadKnownVMs(jvmcfg, JNI_FALSE) < 1) {JLI_ReportErrorMessage(CFG_ERROR7);exit(1);}jvmpath[0] = '\0';jvmtype = CheckJvmType(pargc, pargv, JNI_FALSE);if (JLI_StrCmp(jvmtype, "ERROR") == 0) {JLI_ReportErrorMessage(CFG_ERROR9);exit(4);}if (!GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath, arch, 0 )) {JLI_ReportErrorMessage(CFG_ERROR8, jvmtype, jvmpath);exit(4);}/** we seem to have everything we need, so without further ado* we return back, otherwise proceed to set the environment.*/mustsetenv = RequiresSetenv(wanted, jvmpath);JLI_TraceLauncher("mustsetenv: %s\n", mustsetenv ? "TRUE" : "FALSE");if (mustsetenv == JNI_FALSE) {JLI_MemFree(newargv);return;}JLI_MemFree(newargv);return;} else { /* do the same speculatively or exit */if (running != wanted) {/* Find out where the JRE is that we will be using. */if (!GetJREPath(jrepath, so_jrepath, GetArchPath(wanted), JNI_TRUE)) {/* give up and let other code report error message */JLI_ReportErrorMessage(JRE_ERROR2, wanted);exit(1);}JLI_Snprintf(jvmcfg, so_jvmcfg, "%s%slib%s%s%sjvm.cfg",jrepath, FILESEP, FILESEP, GetArchPath(wanted), FILESEP);/** Read in jvm.cfg for target data model and process vm* selection options.*/if (ReadKnownVMs(jvmcfg, JNI_TRUE) < 1) {/* give up and let other code report error message */JLI_ReportErrorMessage(JRE_ERROR2, wanted);exit(1);}jvmpath[0] = '\0';jvmtype = CheckJvmType(pargc, pargv, JNI_TRUE);if (JLI_StrCmp(jvmtype, "ERROR") == 0) {JLI_ReportErrorMessage(CFG_ERROR9);exit(4);}/* exec child can do error checking on the existence of the path */jvmpathExists = GetJVMPath(jrepath, jvmtype, jvmpath, so_jvmpath, GetArchPath(wanted), 0);mustsetenv = RequiresSetenv(wanted, jvmpath);}if (mustsetenv) {/** We will set the LD_LIBRARY_PATH as follows:** o $JVMPATH (directory portion only)* o $JRE/lib/$LIBARCHNAME* o $JRE/../lib/$LIBARCHNAME** followed by the user's previous effective LD_LIBRARY_PATH, if* any.*//* runpath contains current effective LD_LIBRARY_PATH setting */jvmpath = JLI_StringDup(jvmpath);new_runpath = JLI_MemAlloc(((runpath != NULL) ? JLI_StrLen(runpath) : 0) +2 * JLI_StrLen(jrepath) + 2 * JLI_StrLen(arch) +JLI_StrLen(jvmpath) + 52);newpath = new_runpath + JLI_StrLen("LD_LIBRARY_PATH=");/** Create desired LD_LIBRARY_PATH value for target data model.*/{/* remove the name of the .so from the JVM path */lastslash = JLI_StrRChr(jvmpath, '/');if (lastslash)*lastslash = '\0';sprintf(new_runpath, "LD_LIBRARY_PATH=""%s:""%s/lib/%s:""%s/../lib/%s",jvmpath,jrepath, GetArchPath(wanted),jrepath, GetArchPath(wanted));/** Check to make sure that the prefix of the current path is the* desired environment variable setting, though the RequiresSetenv* checks if the desired runpath exists, this logic does a more* comprehensive check.*/if (runpath != NULL &&JLI_StrNCmp(newpath, runpath, JLI_StrLen(newpath)) == 0 &&(runpath[JLI_StrLen(newpath)] == 0 || runpath[JLI_StrLen(newpath)] == ':') &&(running == wanted) /* data model does not have to be changed */) {JLI_MemFree(newargv);JLI_MemFree(new_runpath);return;}}/** Place the desired environment setting onto the prefix of* LD_LIBRARY_PATH. Note that this prevents any possible infinite* loop of execv() because we test for the prefix, above.*/if (runpath != 0) {JLI_StrCat(new_runpath, ":");JLI_StrCat(new_runpath, runpath);}if (putenv(new_runpath) != 0) {exit(1); /* problem allocating memory; LD_LIBRARY_PATH not setproperly */}/** Unix systems document that they look at LD_LIBRARY_PATH only* once at startup, so we have to re-exec the current executable* to get the changed environment variable to have an effect.*/newenvp = environ;}{char *newexec = execname;JLI_TraceLauncher("TRACER_MARKER:About to EXEC\n");(void) fflush(stdout);(void) fflush(stderr);if (mustsetenv) {execve(newexec, argv, newenvp);} else {execv(newexec, argv);}execv(newexec, argv);JLI_ReportErrorMessageSys(JRE_ERROR4, newexec);}exit(1);} }GetJREPath方法:
java_md_solinux.c
static jboolean GetJREPath(char *path, jint pathsize, const char * arch, jboolean speculative) {char libjava[MAXPATHLEN];if (GetApplicationHome(path, pathsize)) { // 讀取到路徑/* Is JRE co-located with the application? */JLI_Snprintf(libjava, sizeof(libjava), "%s/lib/%s/" JAVA_DLL, path, arch);if (access(libjava, F_OK) == 0) {JLI_TraceLauncher("JRE path is %s\n", path);return JNI_TRUE;}/* Does the app ship a private JRE in <apphome>/jre directory? */JLI_Snprintf(libjava, sizeof(libjava), "%s/jre/lib/%s/" JAVA_DLL, path, arch);if (access(libjava, F_OK) == 0) {JLI_StrCat(path, "/jre");JLI_TraceLauncher("JRE path is %s\n", path);return JNI_TRUE;}}if (!speculative)JLI_ReportErrorMessage(JRE_ERROR8 JAVA_DLL);return JNI_FALSE; }也即,最終要找到 libjvm.so文件(windows下要找到 jvm.dll)
要去操作jvm,那么就需要有函數指針去操作jvm:
而這個結構體是在加載jvm的時候使用到了:
jboolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn) {void *libjvm;JLI_TraceLauncher("JVM path is %s\n", jvmpath);// 打開動態鏈接庫 libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL);if (libjvm == NULL) {JLI_ReportErrorMessage(DLL_ERROR1, __LINE__);JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());return JNI_FALSE;}ifn->CreateJavaVM = (CreateJavaVM_t)dlsym(libjvm, "JNI_CreateJavaVM");if (ifn->CreateJavaVM == NULL) {JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());return JNI_FALSE;}ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t)dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs");if (ifn->GetDefaultJavaVMInitArgs == NULL) {JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());return JNI_FALSE;}ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t)dlsym(libjvm, "JNI_GetCreatedJavaVMs");if (ifn->GetCreatedJavaVMs == NULL) {JLI_ReportErrorMessage(DLL_ERROR2, jvmpath, dlerror());return JNI_FALSE;}return JNI_TRUE; }加載結束后,就開始初始化jvm了
也就是執行JVMInit方法
OS欲啟動java進程,就需要將libjvm動態鏈接庫里的數據加載出來,并且我們希望創建出來的Java線程要能夠可控(調整參數,創建新的線程,不使用原來的線程,然后用新創建出來的線程進行處理),因此我們會在原來的線程上使用pthread_create創建出新的真正用于Java的主線程
在JavaMain的實現體中,有如下代碼
也即需要去看InitializeJVM方法具體是如何初始化jvm的
其中的ifn->CreateJavaVM(pvm, (void **)penv, &args);則是將jvm啟動起來了
malloc 占用c堆內存 開辟 Java堆內存
所以: Java堆內內存:Xmx參數指定的malloc開辟的jvm的自己工作空間
? Java堆外內存:原來的c堆內存
JVM通過傳遞給原生C一個錨點,通過該錨點向堆內存中讀寫數據,并調用libjvm中的函數指針,進而不需要再次通過dlsym進行符號解析
hotspot想要加載字節碼,就需要用到類加載器,而類加載器又是需要通過hotspot進行加載的,所以告訴我們需要引入一個東西來幫忙加載類加載器,而這個東西就是類庫
由于任何一門高級語言都會有自己的庫函數以簡化開發,同理,Java語言也應有自己的庫函數,而對于Java語言來講,其自己本身就是一個庫函數
而對于Java來講,所有的類都需要類加載器進行加載,上面說了Java本身是一個庫函數也即類庫,那么Java語言本身也就該通過類加載器進行加載
那么如果只有一個類加載器的話,我可以通過這種方式進行操作
因此我們需要有一種保護機制,來使得用戶的應用中寫的庫函數不會將原來的系統庫函數替換掉
于是為了解決這個問題,需要有多個類加載器,在Java中的類加載器使用了層級結構
No.1只加載系統庫函數,而應用程序只能通過No.3的類加載器去加載,這樣就防止了應用程序將系統庫函數覆蓋
也就是只需要讓父類加載器永遠優先于子類加載器加載即可
當我創建多個業務類的類加載器,只需要共享同一個頂級的父類加載器
將類的加載交由父類優先去加載,如果父類無法加載再向下傳遞,這便是雙親委派機制。
每一個類加載器都是通過哈希表的形式來映射類的,如果當類一樣,方法名也一樣的時候,如何加載?建立多個類加載器啊
而這個key-value的格式就是 SystemDictionary 其中 key就是代表類加載器,value就是應該要加載的類
對于SystemDictionary的描述為:
The system dictionary stores all loaded classes and maps:
[class name,class loader] -> class i.e. [Symbol* ,oop] -> Klass*
Classes are loaded lazily. The default VM class loader is
represented as NULL.
The underlying data structure is an open hash table with a fixed number
of buckets. During loading the loader object is locked, (for the VM loader
a private lock object is used). Class loading can thus be done concurrently,
but only by different loaders.
During loading a placeholder (name, loader) is temporarily placed in
a side data structure, and is used to detect ClassCircularityErrors
and to perform verification during GC. A GC can occur in the midst
of class loading, as we call out to Java, have to take locks, etc.
When class loading is finished, a new entry is added to the system
dictionary and the place holder is removed. Note that the protection
domain field of the system dictionary has not yet been filled in when
the “real” system dictionary entry is created.
Clients of this class who are interested in finding if a class has
been completely loaded – not classes in the process of being loaded –
can read the SystemDictionary unlocked. This is safe because
Note that placeholders are deleted at any time, as they are removed
when a class is completely loaded. Therefore, readers as well as writers
of placeholders must hold the SystemDictionary_lock.
第一句話我們就知道了 SystemDictionary存儲了所有加載的類和映射信息
[類名,類加載器] -> class 也即 [Symbol ,oop] -> Klass**
使用以上的兩種方式: 層級結構保證了安全性,哈希表結構保證了隔離性
而上面的這一套機制需要有一個東西來承載實現,也就是要通過類庫來加載這個機制
因此我們可以在JavaMain方法中找到
兩大核心文件jvm.cpp和jni.cpp的差異:
jni.cpp (Java Native Interface)用于在Java層面調C語言,也就是Java與C/C++語言溝通的橋梁
jvm.cpp java內部的功能函數,可以通過符號調用 內建核心函數
所以我們在看 findBootClass函數的時候,也就是要找 JVM_FindClassFromBootLoader的函數調用,所以需要在jvm.cpp中查找
據以上代碼我們可以看到,并沒有BootLoader對象
也就是說,在于hotspot中的BootLoader實際上是不存在的,這是一個理論上的類加載器,而在c++層面想要加載程序并不一定非得要用到加載器類,而是可以通過c++代碼的方式加載類
jvm是c++寫的,那我直接在類庫中寫c++代碼加載的Java類算不算ClassLoader?因此無法通過Java層面getBootLoader,也即BootLoader類不管是在Java層面還在C++層面都沒有這個類的實現
至此,我們加載了Java中的LauncherHelper這個類
而我們知道c++是如何創建一個類的,以及Java創建類的過程
所以我們可以推斷Java對象的創建可以與c++類比
在c++的類代碼中,static是當前文件共享,而Java的static是在類的空間中,static修飾的不屬于對象,而是所有對象共享
上圖中,Java由于靜態變量隸屬于類,所以需要類的構造器,而成員變量屬于對象,則需要對象構造器
我們在寫Java的時候經常會遇到static{}在大括號中賦值的寫法(靜態代碼塊),這就是Java的類構造器,而平常寫的構造函數則是對象的構造器
因此:類構造器打包static{}和靜態變量賦值語句,按順序排列 -> 由JVM對clinit類構造器的定義
public class A {static{}public static void main(String[] args) {} } Classfile /H:/source_code/openjdk8_debug/target/classes/hahaha/xjk/xx/A.classLast modified 2022-10-6; size 424 bytesMD5 checksum 28df4b7f385a582580cb963ff4a2441cCompiled from "A.java" public class hahaha.xjk.xx.Aminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #3.#18 // java/lang/Object."<init>":()V#2 = Class #19 // hahaha/xjk/xx/A#3 = Class #20 // java/lang/Object#4 = Utf8 <init>#5 = Utf8 ()V#6 = Utf8 Code#7 = Utf8 LineNumberTable#8 = Utf8 LocalVariableTable#9 = Utf8 this#10 = Utf8 Lhahaha/xjk/xx/A;#11 = Utf8 main#12 = Utf8 ([Ljava/lang/String;)V#13 = Utf8 args#14 = Utf8 [Ljava/lang/String;#15 = Utf8 <clinit>#16 = Utf8 SourceFile#17 = Utf8 A.java#18 = NameAndType #4:#5 // "<init>":()V#19 = Utf8 hahaha/xjk/xx/A#20 = Utf8 java/lang/Object {public hahaha.xjk.xx.A();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 8: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lhahaha/xjk/xx/A;public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=0, locals=1, args_size=10: returnLineNumberTable:line 17: 0LocalVariableTable:Start Length Slot Name Signature0 1 0 args [Ljava/lang/String;static {};descriptor: ()Vflags: ACC_STATICCode:stack=0, locals=0, args_size=00: returnLineNumberTable:line 12: 0 } SourceFile: "A.java"通過對字節碼的分析,發現并沒有clinit方法,因此可以推測clinit方法是在Java虛擬機中實現并執行的
在前面的hotspot中看到了加載了一個LauncherHelper類,這個Java類中沒有靜態代碼塊,并且也沒有這個類的main方法
如何執行的這個類呢?
也就是執行了LaucherHelper中的checkAndLoadMain方法
在JavaMain中先執行了LoadMainClass,也就是加載并調用了LauncherHelper中的checkAndLoadMain方法
然后再通過函數指針調main方法
執行java的main方法
綜上所述,
LauncherHelper類作為Java層面的被加載的第一個類,提供了加載字節碼和其他功能函數
JNIEnv提供了C與JVM的交互方式
而bootLoader直接用c++代碼編寫,加載LauncherHelper,打破了第一個類加載器需要被加載的問題
public static Class<?> checkAndLoadMain(boolean printToStderr,int mode,String what) {initOutput(printToStderr);// get the class nameString cn = null;switch (mode) {case LM_CLASS:cn = what;break;case LM_JAR:cn = getMainClassFromJar(what);break;default:// should never happenthrow new InternalError("" + mode + ": Unknown launch mode");}cn = cn.replace('/', '.');Class<?> mainClass = null;try {mainClass = scloader.loadClass(cn);} catch (NoClassDefFoundError | ClassNotFoundException cnfe) {if (System.getProperty("os.name", "").contains("OS X")&& Normalizer.isNormalized(cn, Normalizer.Form.NFD)) {try {// On Mac OS X since all names with diacretic symbols are given as decomposed it// is possible that main class name comes incorrectly from the command line// and we have to re-compose itmainClass = scloader.loadClass(Normalizer.normalize(cn, Normalizer.Form.NFC));} catch (NoClassDefFoundError | ClassNotFoundException cnfe1) {abort(cnfe, "java.launcher.cls.error1", cn);}} else {abort(cnfe, "java.launcher.cls.error1", cn);}}// set to mainClassappClass = mainClass;/** Check if FXHelper can launch it using the FX launcher. In an FX app,* the main class may or may not have a main method, so do this before* validating the main class.*/if (mainClass.equals(FXHelper.class) ||FXHelper.doesExtendFXApplication(mainClass)) {// Will abort() if there are problems with the FX runtimeFXHelper.setFXLaunchParameters(what, mode);return FXHelper.class;}validateMainClass(mainClass);return mainClass; }這個時候調用了systemClassLoader
而上面的重點代碼便是:
和
那么getLauncher又是如何get的呢?
private ClassLoader loader; public Launcher() {ClassLoader extcl;extcl = ExtClassLoader.getExtClassLoader();loader = AppClassLoader.getAppClassLoader(extcl);Thread.currentThread().setContextClassLoader(loader); }此時可以看到了ExtClassLoader和AppClassLoader
兩個的繼承關系如下:
ClassLoader定義了類加載器的頂層共有最基本的信息
SecureClassLoader引入安全機制,決定哪些類可以加載哪些類不可以被加載
URLClassLoader提供文件名,通過給定的路徑或約定的路徑去尋找類
ExtClassLoader擴展類加載器
AppClassLoader具體應用類加載器
那么我們要去研究ClassLoader的最基本的方法,也就是loadClass,因此需要通過ClassLoader這個類去看看他的方法定義。
將父類構造器作為一個成員放入類中,默認為SystemClassLoader
通過父類去加載,如果父類構造器沒有則去通過BootStrap加載(C++本地方法)
如果都沒找到則讓子類去實現findClass
對于我們現在考慮某個類的加載而言,暫時無需關心安全問題,直接跳到URLClassLoader中
替換.成/,并在末尾添加.class 最終通過URLClassPath加載
而ExtClassLoader應該為AppClassLoader的parent,不通過繼承關系獲得,而是使用設置值的方式,執行loadClass的時候先讓parent去加載
同理,那么我想要自己實現一個類加載器如何做?
繼承 URLClassLoader或者ClassLoader,實現loadClass方法,將AppClassLoader作為parent,并實現自己的findClass方法
ExtClassLoader
AppClassLoader
而核心文件,如rt.jar包的內容由BootstrapClassLoader(C++代碼)直接加載
這里需要注意的是在Launcher中有一個SystemClassLoader,這個也就是AppClassLoader
了解了整個加載過程,那么,main函數最終經過這幾個步驟便完成了Java main的加載,
調用由虛擬機調用,因此我們需要去研究jvm是如何啟動的
因為jni.cpp是連通Java與c/cpp的橋梁,但凡外部需要與jvm溝通就必須經過jni
而在java.c中我們看到的只有一個LoadJavaVM方法中有一個JNI_CreatJavaVM
也就是意味著,這個時候將jni中的Java虛擬機創建并動態鏈接過來,在invoke查找符號的時候找到的CreateJavaVM
并且在JavaMain中
也就是ifn引用的需要通過JNI_XXX去查找在jni.cpp中 如下代碼:
_JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) jint result = JNI_ERR;result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);return result; }因此研究create的過程需要找到JNI下的createVM方法
Java虛擬機是模擬了計算機的所有信息
既然是虛擬,那么肯定與OS有關聯關系,也必然與CPU 指令集相關
第一步肯定是需要操作系統的相關信息,所有系統初始化第一步必然要讀取操作系統的所有信息
// this is called _before_ the most of global arguments have been parsed void os::init(void) {char dummy; /* used to get a guess on initial stack address */ // first_hrtime = gethrtime();// With LinuxThreads the JavaMain thread pid (primordial thread)// is different than the pid of the java launcher thread.// So, on Linux, the launcher thread pid is passed to the VM// via the sun.java.launcher.pid property.// Use this property instead of getpid() if it was correctly passed.// See bug 6351349.pid_t java_launcher_pid = (pid_t) Arguments::sun_java_launcher_pid();_initial_pid = (java_launcher_pid > 0) ? java_launcher_pid : getpid();clock_tics_per_sec = sysconf(_SC_CLK_TCK);init_random(1234567);ThreadCritical::initialize();Linux::set_page_size(sysconf(_SC_PAGESIZE));if (Linux::page_size() == -1) {fatal(err_msg("os_linux.cpp: os::init: sysconf failed (%s)",strerror(errno)));}init_page_sizes((size_t) Linux::page_size());Linux::initialize_system_info();// main_thread points to the aboriginal threadLinux::_main_thread = pthread_self();Linux::clock_init();initial_time_count = javaTimeNanos();// pthread_condattr initialization for monotonic clockint status;pthread_condattr_t* _condattr = os::Linux::condAttr();if ((status = pthread_condattr_init(_condattr)) != 0) {fatal(err_msg("pthread_condattr_init: %s", strerror(status)));}// Only set the clock if CLOCK_MONOTONIC is availableif (Linux::supports_monotonic_clock()) {if ((status = pthread_condattr_setclock(_condattr, CLOCK_MONOTONIC)) != 0) {if (status == EINVAL) {warning("Unable to use monotonic clock with relative timed-waits" \" - changes to the time-of-day clock may have adverse affects");} else {fatal(err_msg("pthread_condattr_setclock: %s", strerror(status)));}}}// else it defaults to CLOCK_REALTIMEpthread_mutex_init(&dl_mutex, NULL);// If the pagesize of the VM is greater than 8K determine the appropriate// number of initial guard pages. The user can change this with the// command line arguments, if needed.if (vm_page_size() > (int)Linux::vm_default_page_size()) {StackYellowPages = 1;StackRedPages = 1;StackShadowPages = round_to((StackShadowPages*Linux::vm_default_page_size()), vm_page_size()) / vm_page_size();} }再OS相關初始化完畢后需要處理參數信息
由于
是將系統的變量注入到全局的system_properties中去
這個時候是在創建Java虛擬機過程中,所以并沒有java的System類,所以需要有C++類來暫時承載system_properties
由這個全局變量保存整個system_property
而Java的System是如何獲取system properties的呢?
而getProperty 的Java代碼如下
然后從props里面拿數據,而誰往props里面填入數據的呢?
肯定是初始化的時候填入的數據嘛
有native關鍵字修飾,那么就是jni的本地函數
而這個initProperties是線程啟動的時候自動調用
之前看到的Java和jni之間的交互是需要用到JNIEnv作為中介的
那么Java語言中用native修飾的方法該怎么找到對應的C語言實現呢?
第1步,找到Java語言native修飾的方法所在的類名,類名.c就是對該方法的實現
第2步,將類所在的 包名.類名.方法名 中的 .替換成_ ,前面補上Java_
即可找到對應的本地方法實現
比如,現在我需要找initProperties的C語言實現方法
那么就要先找到system.c,然后找到 Java_java_lang_System_initProperties
如圖:
原因是:
jvm需要做如下工作
1、dlopen打開動態鏈接庫
2、按照規約 dlsym ,通過規約符號找到符號函數地址
3、保存鏈接庫的引用(后續可以使用)
4、調用獲取到的函數地址
那么為什么要定義這個規約呢?
如果不定義規約,你按照你的方式去找,我按照我的定義去寫,最終對應不上,那你虛擬機也就找不到對應的Java方法
因此我們需要回過頭來看看Java_java_lang_System_initProperties方法里面具體干了什么事
對于GetJavaProperties函數返回了一個內部用于保存所有properties數據的結構體
再通過JNIEnv調用Java中的put,remove,getProperty等方法,也即通過調用到了Java中的Properties對象
Java層面:在initProperties中需要傳入Properties對象
那么在執行initProperties之前需要先存在有Properties對象,而這個對象需要通過JNIEnv調用Java的Properties對象屬性獲取
于是在c層面, 使用了(*env)->GetMethodID調用Properties,通過c代碼可以知道,調用了Properties的 put,remove,getProperty,找到對應方法的地址
通過Java代碼可以知道Properties繼承自Hashtable,所以必定有put remove方法,而getProperty方法按照JNIEnv寫法應該是獲取這個方法的函數地址:
那么這個時候就需要知道,到底是誰,在哪兒將property傳入進去了呢?
之前我們看到了在Threads::createVM方法中執行了
方法,而我們需要思考的是,init_system_properties確實是設置了一個全局的系統配置,也即
那么誰調用了 _system_properties呢?
誰調用了system_properties呢?我們可以找到jvm.cpp中有對此的調用
這個for循環的調用來自于
所以也就是當C語言調用JVM_InitProperties方法的時候會進入到這個地方
誰調用了JVM_InitProperties方法?返回去看Java_java_lang_System_initProperties方法,這其中就調用了JVM_InitProperties方法,
回到createVM的流程中,
這段處理,都是初始化系統屬性
將很多系統的屬性插入到了Arguments中
那么下一步應該是需要轉換一些參數解析處理了吧
如下,將前面設置好的系統屬性轉換成jvm的系統參數進行解析
然后需要在每一個hotspot模塊初始化之前需要初始化大頁(如果頁小,則頁表項多,所以需要分配一部分大頁內存)
經過其他處理,進行OS初始化第二階段
os::init_2方法中存在一個polling page,而前面的page內存經過設置,這里使用mmap進行內存分配
思考一個問題:如何將一系列Java線程停下來?
Java線程通過檢測某個標志位,當出現某個標志的時候線程自己停下來即可。這個標志位如何做呢?
設置一個不可訪問的頁,當線程訪問到這個頁就報錯,進而自動停下來。
在什么時候檢測信號標志位呢?在線程安全的時候去檢測嘛,也即 線程安全點(safe point)
完成init_2之后,需要再次調整參數存儲
進行TLS初始化
之后會進行一些處理,之后,createVM會執行到此處:
_thread_list 實現Java線程列表,所有創建的Java線程均保存在這個列表中
_number_of_threads 線程列表中線程的個數
_number_of_non_daemon_threads 線程列表中非守護線程的個數,當這里面的線程個數為0時,主線程結束,所有線程都結束
在堆中初始化全局數據結構和創建系統類(主函數代碼)
繼續上面的,createVM的過程:
這個main_thread也就是大家所理解的Java主線程
如果沒有JavaThread,如何表達當前Java線程所處的狀態信息?因為這個狀態不屬于TCB,也不屬于PCB,用于表示當前線程正在執行虛擬機的代碼
而main_thread表示當前線程的執行體
所以這個就是用于承載 在jvm層面上對于Java線程對象的信息
下面大概講講JavaThread創建的過程
根據jni的規范,我們知道在Java中 new Thread().start()的jni應該是在Thread.c中
而我們知道JVM_開頭的內建核心函數是在jvm.cpp中
其中有一行代碼如下
通過函數指針執行了thread_entry方法,而thread_entry干了什么事呢?
其中可以看到 vmSymbols::run_method_name(),說明,反向調用了run方法
而set_thread_state()方法傳入的是線程狀態,因此我們需要去看看Java線程真實的狀態是哪些?
這個JavaThreadState是對整個Java線程所有的狀態
除此之外,Java在執行字節碼的時候執行的狀態應該是:
繼續create_vm
初始化全局模塊的信息
jint init_globals() {HandleMark hm;management_init(); // 初始化管理模塊bytecodes_init(); // 初始化字節碼信息classLoader_init(); // 初始化類加載器codeCache_init(); // 初始化代碼緩存VM_Version_init(); // 初始化VM版本信息os_init_globals(); // 初始化OS全局信息stubRoutines_init1(); // 代碼路由第一步初始化jint status = universe_init(); // 初始化GC相關信息(依賴于codeCache_init和stubRoutines_init1和metaspace_init)if (status != JNI_OK) // 如果初始化失敗,返回狀態信息return status;interpreter_init(); // 初始化解釋器(在所有方法加載之前)invocationCounter_init(); // 初始化調用程序計數器(在所有方法加載之前)marksweep_init(); // 初始化標記清除垃圾回收相關信息accessFlags_init(); // 初始化訪問修飾符模塊templateTable_init(); // 初始化字節碼模板表InterfaceSupport_init(); // 初始化接口支持模塊SharedRuntime::generate_stubs(); // 初始化代碼調用stubuniverse2_init(); // 對GC相關信息進行二次初始化(依賴于codeCache_init和stubRoutines_init1)referenceProcessor_init(); // 初始化引用處理jni_handles_init(); // 初始化JNI回調模塊 #if INCLUDE_VM_STRUCTSvmStructs_init(); // vm結構體初始化 #endif // INCLUDE_VM_STRUCTSvtableStubs_init(); // 初始化虛擬方法表模塊InlineCacheBuffer_init(); // 初始化內聯代碼緩沖模塊compilerOracle_init(); // 初始化orcale設置的編譯器compilationPolicy_init(); // 初始化編譯策略模塊compileBroker_init(); // 初始化編譯管理器模塊VMRegImpl::set_regName();if (!universe_post_init()) { // 對universe也即gc相關進行鉤子回調return JNI_ERR;}javaClasses_init(); // 初始化java類信息 (必須在虛擬方法表初始化之后)stubRoutines_init2(); // 對代碼路由二次初始化(注意: StubRoutines需要第二階段的初始化)// 所有由VM_Version_init和os::init_2調整的標志都已經設置,所以現在轉儲標志。if (PrintFlagsFinal) { // 如果設置了PrintFlagsFinal為true,那么在這里打印所有參數信息CommandLineFlags::printFlags(tty, false);}return JNI_OK; }上面的 init_globals方法中的bytecode_init就是將Java字節碼進行初始化
定義了203個字節碼信息
所以每個字節碼定義的長度都是32位或者64位
可以對應著和RISC學習
每個字節碼長度一致
enum Code {_illegal = -1,// Java bytecodes_nop = 0, // 0x00_aconst_null = 1, // 0x01_iconst_m1 = 2, // 0x02_iconst_0 = 3, // 0x03_iconst_1 = 4, // 0x04_iconst_2 = 5, // 0x05_iconst_3 = 6, // 0x06_iconst_4 = 7, // 0x07_iconst_5 = 8, // 0x08_lconst_0 = 9, // 0x09_lconst_1 = 10, // 0x0a_fconst_0 = 11, // 0x0b_fconst_1 = 12, // 0x0c_fconst_2 = 13, // 0x0d_dconst_0 = 14, // 0x0e_dconst_1 = 15, // 0x0f_bipush = 16, // 0x10_sipush = 17, // 0x11_ldc = 18, // 0x12_ldc_w = 19, // 0x13_ldc2_w = 20, // 0x14_iload = 21, // 0x15_lload = 22, // 0x16_fload = 23, // 0x17_dload = 24, // 0x18_aload = 25, // 0x19_iload_0 = 26, // 0x1a_iload_1 = 27, // 0x1b_iload_2 = 28, // 0x1c_iload_3 = 29, // 0x1d_lload_0 = 30, // 0x1e_lload_1 = 31, // 0x1f_lload_2 = 32, // 0x20_lload_3 = 33, // 0x21_fload_0 = 34, // 0x22_fload_1 = 35, // 0x23_fload_2 = 36, // 0x24_fload_3 = 37, // 0x25_dload_0 = 38, // 0x26_dload_1 = 39, // 0x27_dload_2 = 40, // 0x28_dload_3 = 41, // 0x29_aload_0 = 42, // 0x2a_aload_1 = 43, // 0x2b_aload_2 = 44, // 0x2c_aload_3 = 45, // 0x2d_iaload = 46, // 0x2e_laload = 47, // 0x2f_faload = 48, // 0x30_daload = 49, // 0x31_aaload = 50, // 0x32_baload = 51, // 0x33_caload = 52, // 0x34_saload = 53, // 0x35_istore = 54, // 0x36_lstore = 55, // 0x37_fstore = 56, // 0x38_dstore = 57, // 0x39_astore = 58, // 0x3a_istore_0 = 59, // 0x3b_istore_1 = 60, // 0x3c_istore_2 = 61, // 0x3d_istore_3 = 62, // 0x3e_lstore_0 = 63, // 0x3f_lstore_1 = 64, // 0x40_lstore_2 = 65, // 0x41_lstore_3 = 66, // 0x42_fstore_0 = 67, // 0x43_fstore_1 = 68, // 0x44_fstore_2 = 69, // 0x45_fstore_3 = 70, // 0x46_dstore_0 = 71, // 0x47_dstore_1 = 72, // 0x48_dstore_2 = 73, // 0x49_dstore_3 = 74, // 0x4a_astore_0 = 75, // 0x4b_astore_1 = 76, // 0x4c_astore_2 = 77, // 0x4d_astore_3 = 78, // 0x4e_iastore = 79, // 0x4f_lastore = 80, // 0x50_fastore = 81, // 0x51_dastore = 82, // 0x52_aastore = 83, // 0x53_bastore = 84, // 0x54_castore = 85, // 0x55_sastore = 86, // 0x56_pop = 87, // 0x57_pop2 = 88, // 0x58_dup = 89, // 0x59_dup_x1 = 90, // 0x5a_dup_x2 = 91, // 0x5b_dup2 = 92, // 0x5c_dup2_x1 = 93, // 0x5d_dup2_x2 = 94, // 0x5e_swap = 95, // 0x5f_iadd = 96, // 0x60_ladd = 97, // 0x61_fadd = 98, // 0x62_dadd = 99, // 0x63_isub = 100, // 0x64_lsub = 101, // 0x65_fsub = 102, // 0x66_dsub = 103, // 0x67_imul = 104, // 0x68_lmul = 105, // 0x69_fmul = 106, // 0x6a_dmul = 107, // 0x6b_idiv = 108, // 0x6c_ldiv = 109, // 0x6d_fdiv = 110, // 0x6e_ddiv = 111, // 0x6f_irem = 112, // 0x70_lrem = 113, // 0x71_frem = 114, // 0x72_drem = 115, // 0x73_ineg = 116, // 0x74_lneg = 117, // 0x75_fneg = 118, // 0x76_dneg = 119, // 0x77_ishl = 120, // 0x78_lshl = 121, // 0x79_ishr = 122, // 0x7a_lshr = 123, // 0x7b_iushr = 124, // 0x7c_lushr = 125, // 0x7d_iand = 126, // 0x7e_land = 127, // 0x7f_ior = 128, // 0x80_lor = 129, // 0x81_ixor = 130, // 0x82_lxor = 131, // 0x83_iinc = 132, // 0x84_i2l = 133, // 0x85_i2f = 134, // 0x86_i2d = 135, // 0x87_l2i = 136, // 0x88_l2f = 137, // 0x89_l2d = 138, // 0x8a_f2i = 139, // 0x8b_f2l = 140, // 0x8c_f2d = 141, // 0x8d_d2i = 142, // 0x8e_d2l = 143, // 0x8f_d2f = 144, // 0x90_i2b = 145, // 0x91_i2c = 146, // 0x92_i2s = 147, // 0x93_lcmp = 148, // 0x94_fcmpl = 149, // 0x95_fcmpg = 150, // 0x96_dcmpl = 151, // 0x97_dcmpg = 152, // 0x98_ifeq = 153, // 0x99_ifne = 154, // 0x9a_iflt = 155, // 0x9b_ifge = 156, // 0x9c_ifgt = 157, // 0x9d_ifle = 158, // 0x9e_if_icmpeq = 159, // 0x9f_if_icmpne = 160, // 0xa0_if_icmplt = 161, // 0xa1_if_icmpge = 162, // 0xa2_if_icmpgt = 163, // 0xa3_if_icmple = 164, // 0xa4_if_acmpeq = 165, // 0xa5_if_acmpne = 166, // 0xa6_goto = 167, // 0xa7_jsr = 168, // 0xa8_ret = 169, // 0xa9_tableswitch = 170, // 0xaa_lookupswitch = 171, // 0xab_ireturn = 172, // 0xac_lreturn = 173, // 0xad_freturn = 174, // 0xae_dreturn = 175, // 0xaf_areturn = 176, // 0xb0_return = 177, // 0xb1_getstatic = 178, // 0xb2_putstatic = 179, // 0xb3_getfield = 180, // 0xb4_putfield = 181, // 0xb5_invokevirtual = 182, // 0xb6_invokespecial = 183, // 0xb7_invokestatic = 184, // 0xb8_invokeinterface = 185, // 0xb9_invokedynamic = 186, // 0xba // if EnableInvokeDynamic_new = 187, // 0xbb_newarray = 188, // 0xbc_anewarray = 189, // 0xbd_arraylength = 190, // 0xbe_athrow = 191, // 0xbf_checkcast = 192, // 0xc0_instanceof = 193, // 0xc1_monitorenter = 194, // 0xc2_monitorexit = 195, // 0xc3_wide = 196, // 0xc4_multianewarray = 197, // 0xc5_ifnull = 198, // 0xc6_ifnonnull = 199, // 0xc7_goto_w = 200, // 0xc8_jsr_w = 201, // 0xc9_breakpoint = 202, // 0xcanumber_of_java_codes,// JVM bytecodes_fast_agetfield = number_of_java_codes,_fast_bgetfield ,_fast_cgetfield ,_fast_dgetfield ,_fast_fgetfield ,_fast_igetfield ,_fast_lgetfield ,_fast_sgetfield ,_fast_aputfield ,_fast_bputfield ,_fast_cputfield ,_fast_dputfield ,_fast_fputfield ,_fast_iputfield ,_fast_lputfield ,_fast_sputfield ,_fast_aload_0 ,_fast_iaccess_0 ,_fast_aaccess_0 ,_fast_faccess_0 ,_fast_iload ,_fast_iload2 ,_fast_icaload ,_fast_invokevfinal ,_fast_linearswitch ,_fast_binaryswitch ,// special handling of oop constants:_fast_aldc ,_fast_aldc_w ,_return_register_finalizer ,// special handling of signature-polymorphic methods:_invokehandle ,_shouldnotreachhere, // For debugging// Platform specific JVM bytecodes #ifdef TARGET_ARCH_x86 # include "bytecodes_x86.hpp" #endif #ifdef TARGET_ARCH_sparc # include "bytecodes_sparc.hpp" #endif #ifdef TARGET_ARCH_zero # include "bytecodes_zero.hpp" #endif #ifdef TARGET_ARCH_arm # include "bytecodes_arm.hpp" #endif #ifdef TARGET_ARCH_ppc # include "bytecodes_ppc.hpp" #endifnumber_of_codes};由于字節碼和RISC指令集很像,可以類比學習
那么需要思考一個問題
這個字節碼中,為什么沒有寄存器和內存呢???
那這樣就引來了另外一個問題,Java虛擬機到底虛擬了什么?
必定是虛擬了真實計算機的虛擬環境
根據虛擬機的信息,我們知道如下信息:
字節碼根本不知道會執行在哪個平臺的cpu上,所以寄存器無法定義,因此需要虛擬寄存器
于是,Java虛擬機使用棧來模擬寄存器來操作(ALU)即可
想要用棧來虛擬寄存器的操作,那么虛擬的寄存器是哪些呢?也就是說,哪個地方是虛擬的哪一種寄存器呢?
所以需要用到一個局部變量表來虛擬寄存器,通過局部變量表告訴虛擬機哪些寄存器在哪兒
綜上,想要Java虛擬機來模擬寄存器操作
第一步,使用局部變量表(一個數組)來模擬寄存器
第二步,使用棧來模擬對寄存器的操作
這種寫法便是c++解釋器對字節碼的解釋
一個循環,對每一個字節碼進行判斷解釋
而模板解釋器與C++解釋器不同
雖然Java與平臺無關,但是執行字節碼的主體是虛擬機,所以虛擬機就與平臺有關了
也就是說jvm與CPU平臺和OS高度耦合,并且知道自己是運行在哪個平臺上,所以可以直接使用平臺相關寄存器來執行字節碼以提升性能
所以在templateTable.cpp中就定義了每個字節碼對應的匯編指令操作
例如_iconst_1對應為 iconst方法,參數為1
由于此時模板解釋器與平臺掛鉤,所以我們看 x86_64處理器
將宏展開后如下
void TemplateTable::iconst(int value) {transition(vtos, itos);if (value == 0) {_masm-> xorl(rax, rax);} else {_masm-> movl(rax, value);} }顯而易見,使用了movl / xorl操作 rax寄存器
前面看到的Bytecodes的def方法是干了什么事呢?
也就是將各種的code,name,format等等信息保存到幾個數組中
回到init_globals方法中
初始化一系列counter用于之后的性能分析器
其中,在initialize方法中存在這個方法
load_zip_library();也即,加載了zip庫
那么肯定會調用動態鏈接庫,也肯定會執行dlopen方法
dll_load方法中執行:
通過上面的代碼知道,除了加載了libjvm.so這個虛擬機本身動態鏈接庫之外,還需要加載其他的動態鏈接庫
從編譯的jdk代碼中可以知道需要引入libjava.so等各種動態鏈接庫
繼續init_globals方法
所謂代碼緩存,就是jit在執行的時候用于動態生成的代碼片段
其中,stub就是一個函數指針,指向一個用來執行的代碼段,可以是c++代碼也可以是匯編指令段,有輸入有輸出
ReservedSpace調用了 mmap映射一大段空間,都是虛擬地址,也就是有頁表但是沒有頁表項
initialize方法中調用了os的 預留內存的方法
這個mmap分配的是虛擬內存,沒有頁表項
回到init_globals中
執行到stubRoutines_init1();
什么是stub在前面講過,那么一組stub就是stubRouting
StubRoutines為 被編譯后的匯編代碼段和運行時系統提供入口點。
特定平臺的入口點被定義在特定的平臺的類中
Class 組合:
Note 1: The important thing is a clean decoupling between stub
entry points (interfacing to the whole vm; i.e., 1-to-n
relationship) and stub generators (interfacing only to
the entry points implementation; i.e., 1-to-1 relationship).
This significantly simplifies changes in the generator
structure since the rest of the vm is not affected.
Note 2: stubGenerator_.cpp contains a minimal portion of
machine-independent code; namely the generator calls of
the generator functions that are used platform-independently.
However, it comes with the advantage of having a 1-file
implementation of the generator. It should be fairly easy
to change, should it become a problem later.
Scheme for adding a new entry point:
a) if platform independent: make subsequent changes in the independent files
b) if platform dependent: make subsequent changes in the dependent files
2. add a private instance variable holding the entry point address
3. add a public accessor function to the instance variable
4. implement the corresponding generator function in the platform-dependent
stubGenerator_.cpp file and call the function in generate_all() of that file
CodeBlob用來保存二進制代碼
而 StubRoutines就是如下部分,那么誰給這些屬性賦值的呢?
StubGenerator_generate(&buffer, false); 這行代碼實現對上面屬性的賦值操作
之后的函數調用都會走到這個地方,尤其是invoke的時候
這里的函數指針其實就是一個用來存放地址的東西
以下代碼是stubGenerator_zero.cpp中
也即零匯編代碼(所謂零匯編,就是這個部分是由c++代碼實現而不是通過直接生成匯編指令實現),
因此需要使用ShouldNotCallThisStub()方法,也即將對于需要使用匯編的stub設置為不可達的地址,一旦執行則報錯
在init_globals方法中我們可以看到subroutines的初始化方法有兩個部分
分別是
因為第二部分需要其他部分先初始化,因此將stubRoutines的初始化分為了兩個部分
回到init_globals
jint universe_init() {jint status = Universe::initialize_heap(); //初始化堆內存if (status != JNI_OK) {return status;}Metaspace::global_initialize(); // 初始化元數據空間// Create memory for metadata. Must be after initializing heap for// DumpSharedSpaces.ClassLoaderData::init_null_class_loader_data(); //初始化類加載器的使用數據SymbolTable::create_table(); //創建初始化符號表StringTable::create_table(); // 創建初始化字符串表ClassLoader::create_package_info_table(); // 創建初始化類加載器的包信息表return JNI_OK; }對于initialize_heap方法
jint Universe::initialize_heap() {GenCollectorPolicy *gc_policy;gc_policy = new MarkSweepPolicy();gc_policy->initialize_all(); // 初始化GC算法 Universe::_collectedHeap = new GenCollectedHeap(gc_policy);jint status = Universe::heap()->initialize(); // 初始化內存if (status != JNI_OK) {return status;}return JNI_OK; }所以,兩個分別管理垃圾回收和內存分配的問題
gc_policy->initialize_all(); 方法就是如下
而GenCollectorPolicy繼承于CollectorPolicy
也就是分代GC策略繼承于GC策略
內存整理:適用于對象小,而且對象存活相對較少
那么也就是說,整理算法怕 大對象,對象多
因此,將大對象和存活次數很多直接存放老年代,而一般這種整理算法都是采用復制算法
新生代和老年代的區別?
新生代用于存放存活較少的,空間較小的對象,便于內存的復制整理
老年代存放空間占用大的,存活率很高的對象,允許存在碎片問題
新生代對象什么時候進入老年代?
由于有些對象一直存在,不能讓它在新生代一直來回復制,所以需要讓它達到一定次數之后扔到老年代中,于是需要引入計數GC復制次數(對象年齡),當年齡達到閾值時存放老年代
新生代為什么會需要有S1(from)和S2(to)兩個空間呢?
因為對象需要復制,從剛誕生的分配區之后需要進行復制整理,因此需要劃分個區域可以便于快速復制清除,兩者交換可以立即得到一塊新的空間
但是會浪費內存
因為新生代無法對對象過多的情況進行有效處理,所以對于批量生成的對象或者當s1或者s2區域無法再分配內存時直接將這些對象存放老年代
我們需要看MarkSweepPolicy對于initialize_generations的實現:
void MarkSweepPolicy::initialize_generations() {_generations = NEW_C_HEAP_ARRAY3(GenerationSpecPtr, 2, mtGC, 0, AllocFailStrategy::RETURN_NULL);if (_generations == NULL) {vm_exit_during_initialization("Unable to allocate gen spec");}_generations[0] = new GenerationSpec(Generation::DefNew, _initial_gen0_size, _max_gen0_size);_generations[1] = new GenerationSpec(Generation::MarkSweepCompact, _initial_gen1_size, _max_gen1_size);if (_generations[0] == NULL || _generations[1] == NULL) {vm_exit_during_initialization("Unable to allocate gen spec");} }至此,GC的初始化基本完畢
下面需要初始化堆內存
jint GenCollectedHeap::initialize() {CollectedHeap::pre_initialize();int i;_n_gens = gen_policy()->number_of_generations();size_t gen_alignment = Generation::GenGrain;_gen_specs = gen_policy()->generations();for (i = 0; i < _n_gens; i++) {_gen_specs[i]->align(gen_alignment);}char* heap_address;size_t total_reserved = 0;int n_covered_regions = 0;ReservedSpace heap_rs;size_t heap_alignment = collector_policy()->heap_alignment();heap_address = allocate(heap_alignment, &total_reserved,&n_covered_regions, &heap_rs);_reserved = MemRegion((HeapWord*)heap_rs.base(),(HeapWord*)(heap_rs.base() + heap_rs.size()));_reserved.set_word_size(0);_reserved.set_start((HeapWord*)heap_rs.base());size_t actual_heap_size = heap_rs.size();_reserved.set_end((HeapWord*)(heap_rs.base() + actual_heap_size));_rem_set = collector_policy()->create_rem_set(_reserved, n_covered_regions);set_barrier_set(rem_set()->bs());_gch = this;for (i = 0; i < _n_gens; i++) {ReservedSpace this_rs = heap_rs.first_part(_gen_specs[i]->max_size(), false, false);_gens[i] = _gen_specs[i]->init(this_rs, i, rem_set());heap_rs = heap_rs.last_part(_gen_specs[i]->max_size());}clear_incremental_collection_failed();return JNI_OK; }在universe_init方法中,
Metaspace::global_initialize();是對于Java元數據區的信息初始化
而什么是元數據區呢?為什么要元數據區呢?
在沒有元數據區的時候,所有的Java對象,類信息,字節碼等所有信息全部放在Java堆內存中,對于垃圾回收來講,怎么去區分是對象呢還是一般類信息呢?
這樣導致耦合度太高,那么就需要將Java對象和描述對象,類信息等數據分開,便于垃圾回收專注于對Java對象的處理
因此,元空間保存了 類的信息
而Java堆用于存放Java對象
由于是進程虛擬內存元空間使用的VirtualSpace開辟內存,而且肯定是使用mmap進行空間映射,不用malloc,因為malloc是使用berk指針開辟空間,無法將底部的空間利用起來,出現內存泄漏的情況
ClassLoaderData::init_null_class_loader_data(); 是對于classloader的初始化
類加載器的數據,
類加載器的對象信息
oop就是一個Java對象,因此ClassLoaderData其實就是與Java的ClassLoader綁定在一起的
而init_null_class_loader_data就是初始化bootstrap 類加載器
解釋器初始化:
那么我們看看Interpreter的hpp
可以看到解釋器的最終形態為:
CppInterpreter和TemplateInterpreter
而CC_INTERP_ONLY和NOT_CC_INTERP為宏定義,用編譯期的時候根據編譯配置選擇最終執行的解釋器
由于我們研究c++的,也即零匯編的,那么可以將該class展開
class Interpreter: public CC_INTERP_ONLY(CppInterpreter) NOT_CC_INTERP(TemplateInterpreter) {public:// Debugging/printingstatic InterpreterCodelet* codelet_containing(address pc) { return (InterpreterCodelet*)_code->stub_containing(pc); } #ifndef CPU_ZERO_VM_INTERPRETER_ZERO_HPP #define CPU_ZERO_VM_INTERPRETER_ZERO_HPPpublic:static void invoke_method(Method* method, address entry_point, TRAPS) {((ZeroEntry *) entry_point)->invoke(method, THREAD);}static void invoke_osr(Method* method,address entry_point,address osr_buf,TRAPS) {((ZeroEntry *) entry_point)->invoke_osr(method, osr_buf, THREAD);}public:static int expr_index_at(int i) {return stackElementWords * i;}static int expr_offset_in_bytes(int i) {return stackElementSize * i;}static int local_index_at(int i) {assert(i <= 0, "local direction already negated");return stackElementWords * i;}#endif // CPU_ZERO_VM_INTERPRETER_ZERO_HPP因為我們研究C++的代碼,看繼承關系需要看Cpp
那么也就會調用CppInterpreter的initialized方法
_code 是 StubQueue
StubQueue是一堆函數組成的隊列 也就是解釋器代碼
由于hotspot是一個熱點編譯
當某一個方法執行到某一個次數的時候調用計數器(InvocationCounter)增加數值,到達閾值之后編譯,當長時間不用的時候該值下降,下降到某個閾值的時候再解除編譯
CppInterpreter的初始化:
TemplateInterpreter:
對于StubQueue
運行在解釋器中的一小段代碼
回到對CppInterpreter的研究
看到InterpreterGenerator需要分配對象,那么肯定會調用其構造方法
說明對于Cpp解釋器來講,沒有任何匯編的東西
直接用c++的函數地址,強轉為address
而之前在generate_all中也見到過這種寫法
而address是一個
看到是u_char* 類型,但不重要,之后在使用的時候是可以直接強轉為某一個特定函數的函數原型指針:返回值(*函數名)(入參) 匿名指針:返回值(*)(入參)
如何執行?(返回值(*)(入參))()再需要進行address強轉為該函數-> ``((返回值(*)(入參))address)()`
回到CppInterpreterGenerator::generate_all()中:
memset將對應的空間全部清零
由于Cpp為零匯編,所以我們在調用JNI時,必定直接使用了C++的代碼:dlopen、dlsym等函數,直接調用,對于返回值,直接遵循C++函數調用規范,拿到返回值直接轉化
解釋器需要執行方法:1.JNI相關方法 2.Java和字節碼的相關方法 3.同步代碼 4.同步代碼與1 2組合的
這些方法都需要入口: 入口在哪?函數指針嘛
那么就是找到對應的函數指針入口點賦值即可
根據方法的類型,生成對應解釋器的方法的入口表
于是以后在調方法時只需要 kind方法類型 和 函數的指針(方法入口)
address AbstractInterpreterGenerator::generate_method_entry(AbstractInterpreter::MethodKind kind) {address entry_point = NULL;switch (kind) {case Interpreter::zerolocals:case Interpreter::zerolocals_synchronized:break;case Interpreter::native:entry_point = ((InterpreterGenerator*) this)->generate_native_entry(false);break;case Interpreter::native_synchronized:entry_point = ((InterpreterGenerator*) this)->generate_native_entry(false);break;case Interpreter::empty:entry_point = ((InterpreterGenerator*) this)->generate_empty_entry();break;case Interpreter::accessor:entry_point = ((InterpreterGenerator*) this)->generate_accessor_entry();break;case Interpreter::abstract:entry_point = ((InterpreterGenerator*) this)->generate_abstract_entry();break;case Interpreter::java_lang_math_sin:case Interpreter::java_lang_math_cos:case Interpreter::java_lang_math_tan:case Interpreter::java_lang_math_abs:case Interpreter::java_lang_math_log:case Interpreter::java_lang_math_log10:case Interpreter::java_lang_math_sqrt:case Interpreter::java_lang_math_pow:case Interpreter::java_lang_math_exp:entry_point = ((InterpreterGenerator*) this)->generate_math_entry(kind);break;case Interpreter::java_lang_ref_reference_get:entry_point = ((InterpreterGenerator*)this)->generate_Reference_get_entry();break;default:ShouldNotReachHere();}if (entry_point == NULL)entry_point = ((InterpreterGenerator*) this)->generate_normal_entry(false);return entry_point; }generate_native_entry()
generate_normal_entry()
最終調了CppInterpreter的native_entry和normal_entry
而normal_entry方法最終開始循環執行字節碼:
interpreter的生成器調用method_entry的宏定義,調用generate_method_entry方法,根據kind的類型調用C++函數的定義,把定義的函數指針放到_entry_table數組中。當需要調用的時候直接invoke,拿到當前方法的類型,在數組中匹配,找到對應類型的方法,將方法包裝成Method之后執行normal_entry執行,然后執行main_loop主循環執行所有字節碼
注意,在解釋器初始化中的stub與stubroutines的不一樣
stubroutines的是一堆外部需要調用的函數的指針
而解釋器中的是函數指針都是給解釋器執行jni和Java代碼的
然后最終回到JavaMain中:
這是調用jni代碼
所有的hotspot啟動整理流程總結:
在此感謝 《深入理解Java高并發編程》和《Tomcat源碼全解和架構思維》的作者 黃俊 提供的學習思維和學習方式
總結
以上是生活随笔為你收集整理的Java虚拟机启动整体流程和基础学习(内容很多,不可快餐阅读),推理+源码论证的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Oracle parallel(并行
- 下一篇: 华为荣耀手表GS3 评测怎么样