Java虚拟机启动整体流程和基础学习(内容很多,不可快餐阅读),推理+源码论证
不啰嗦,直接看:
ISA指令強(qiáng)關(guān)聯(lián)CPU平臺(tái),那么匯編語言也一定強(qiáng)關(guān)聯(lián)CPU平臺(tái),而C語言又是對(duì)匯編最簡單的抽象,也就一定會(huì)耦合不同CPU平臺(tái)的代碼,這也就是意味著,在寫C/C++項(xiàng)目的時(shí)候,必須要考慮不同平臺(tái)的代碼編寫差異,要根據(jù)不同的平臺(tái)寫不同的代碼以滿足不同平臺(tái)的功能使用與相關(guān)優(yōu)化。
而我們知道程序想要操作硬件就必須要經(jīng)過操作系統(tǒng),由操作系統(tǒng)給定的系統(tǒng)調(diào)用去進(jìn)行硬件上的操作
OS內(nèi)核是在進(jìn)程的虛擬地址的1GB空間,作為進(jìn)程的內(nèi)核態(tài)來執(zhí)行,也就是可以理解為一串代碼,不是獨(dú)立的執(zhí)行的軟件
也就是說,如果想要一個(gè)App能在不同的操作系統(tǒng)上運(yùn)行,那么必須要有不同的操作系統(tǒng)宏處理或者文件以適配不同系統(tǒng)的系統(tǒng)調(diào)用
除了通過OS的系統(tǒng)調(diào)用操作計(jì)算機(jī)硬件外,還可以通過內(nèi)聯(lián)匯編直接操作CPU
對(duì)于C/C++開發(fā)人員來講,會(huì)有一個(gè)很重要的問題需要去考慮:指針問題
野指針(指向不明空間/無效內(nèi)存),內(nèi)存泄漏(忘記釋放),內(nèi)存溢出等問題
為了規(guī)避這種操作指針操作帶來的問題,那么就有兩種解決方案:
讓編譯器識(shí)別指針問題,但是這個(gè)方式也帶來不足之處,由于如果出現(xiàn)任何指針問題則編譯不通過,使得語言編寫相對(duì)死板,就不能使用一些特殊的寫法讓代碼量減少
自動(dòng)釋放指針,也即垃圾回收機(jī)制。基于此想法,便出現(xiàn)了Java
那么Java編譯器該怎么設(shè)計(jì)呢?
直接生成 ISA指令集?是可以的,但是Java語言開發(fā)者是想要適配多個(gè)不同的CPU平臺(tái),適配不同OS,如果直接生成ISA,那豈不是和其他的靜態(tài)語言一樣了?
那么是將Java編譯生成抽象語法樹,然后用C/C++去識(shí)別和處理這個(gè)抽象語法樹?亦或者是將Java編譯生成一個(gè)中間語言(或稱中間表示)然后交由c/c++去處理呢?
很顯然,Java選擇了后者,為什么呢??動(dòng)態(tài)性,如果選擇抽象語法樹,那么所有的動(dòng)態(tài)性全都需要在c++引擎層面去實(shí)現(xiàn),而使用中間語言則可以讓Java語言與C++脫鉤,只需要C++識(shí)別處理中間語言即可
這個(gè)中間語言便是熟知的 字節(jié)碼
最終便得到了:
用C++代碼解釋字節(jié)碼文件,但是這樣就會(huì)出現(xiàn)一個(gè)問題,c++讀取字節(jié)碼后遍歷字節(jié)碼,然后條件判斷再進(jìn)行不同的處理
但是這樣性能不高,即便C++再怎么優(yōu)化也不如直接生成匯編語言來的快,甚至可以預(yù)先將匯編語言生成好機(jī)器碼,然后直接拿機(jī)器碼去運(yùn)行
因此,就有了如下幾種機(jī)制:
C++解釋字節(jié)碼
字節(jié)碼直接映射成機(jī)器碼執(zhí)行,由機(jī)器碼解釋
動(dòng)態(tài)編譯優(yōu)化(JIT)
運(yùn)行速度:
C++解釋 < 機(jī)器碼解釋 < 編譯優(yōu)化
啟動(dòng)速度:
C++解釋 > 機(jī)器碼解釋 > 編譯優(yōu)化
根據(jù)熱點(diǎn)的方法進(jìn)行選擇性編譯,也就是:
1、按照解釋執(zhí)行啟動(dòng)方法
2、根據(jù)調(diào)用的熱點(diǎn)(Hotspot)方法 運(yùn)行 JIT 動(dòng)態(tài)編譯優(yōu)化
GCC存在 -O1、- O2 、-O3等優(yōu)化等級(jí),每一級(jí)的優(yōu)化速度不同,性能也不同,那么在JIT中引入此方式,得到分層編譯,既滿足編譯速度也執(zhí)行的速度
C++在不同的操作系統(tǒng)上的處理不同,因此圖變?nèi)缦?#xff1a;
使得Java程序無需關(guān)心底層不同OS平臺(tái)是如何處理的,只需要專注于業(yè)務(wù)代碼的編寫即可
也即:Write Once,Run Everywhere,跨平臺(tái)
所以Java只需要面向字節(jié)碼編程即可
那既然要解釋字節(jié)碼,若不同虛擬機(jī)實(shí)現(xiàn)字節(jié)碼的方式不同,就會(huì)回到C/C++不同平臺(tái)不同處理,那么是不是需要有一定的規(guī)范如何去解釋?
《Java虛擬機(jī)規(guī)范》約束不同虛擬機(jī)廠商對(duì)于該字節(jié)碼的約定與解釋
由于字節(jié)碼底層操作的對(duì)象需要處理,那么因此引入了垃圾回收機(jī)制(GC模塊)
無需JAVA程序員做GC的工作,在GC模塊中實(shí)現(xiàn)自動(dòng)垃圾回收
Java 字節(jié)碼解析:
public class Hello{public static void main(String[] args){System.out.println("Hello World");}}我們都知道,在C語言的程序中,函數(shù)傳參是放在棧幀或者寄存器中,字符串存放在.data下的.string 靜態(tài)數(shù)據(jù)域中
那我們可以猜測一下,Java的字節(jié)碼會(huì)不會(huì)和C語言的存放格式差不多呢?
使用javac命令編譯Hello.java得到 Hello.class字節(jié)碼文件,使用javap -v 命令查看字節(jié)碼信息
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文件展示方式一樣啊
那么我們需要解析這個(gè)字節(jié)碼文件,如何解析呢,我得知道排布格式吧?前面說到了《java虛擬機(jī)規(guī)范》定義的啊
規(guī)范定義什么,那底層實(shí)現(xiàn)就是什么,與操作系統(tǒng)和CPU平臺(tái)無關(guān)
也就是我可以通過讀入文件數(shù)據(jù)流,按照以上格式進(jìn)行解析,即可得到字節(jié)碼文件中的相關(guān)信息
那按照這個(gè)虛擬機(jī)規(guī)范,hotspot是怎么解析這個(gè)classfile的呢?
猜測一下,是遍歷整個(gè)文件,然后按照對(duì)應(yīng)的字節(jié)長度進(jìn)行判斷解析
Java 加載,鏈接,初始化
Java沒有靜態(tài)鏈接,只有動(dòng)態(tài)鏈接
而在C語言中,.so文件的GOT表和PLT表進(jìn)行跳轉(zhuǎn)鏈接,那Java中的動(dòng)態(tài)鏈接同樣需要找表
在字節(jié)碼中的constantPool就是ELF文件中的DynamicSymbol
如何做的呢?
讀入代碼段中的數(shù)據(jù),找到invokespecial 對(duì)應(yīng)的 constantPool中數(shù)據(jù)對(duì)應(yīng)的地址解析出來,再把地址填進(jìn)去執(zhí)行即可
由于我們知道,需要按照一定的格式來讀取,那么我們需要考慮將這些數(shù)據(jù)保存起來
也就是需要有個(gè)容器來存放ClassFile Structure,猜測應(yīng)該是放在與ClassFile相關(guān)的c++代碼中
而在Hotspot中沒有直接的classfile.cpp,而與之相關(guān)的只有classFileParser.cpp、classFileStream.cpp前者意思是字節(jié)碼解析器,那么肯定與字節(jié)碼處理相關(guān)的部分的代碼, 而后者是字節(jié)碼文件流,肯定是用來幫助讀取字節(jié)碼數(shù)據(jù)流的,因此我們得看classFileParser.cpp
在其中的parseClassFile方法中
通過查找其函數(shù)體,我們可以找到如下代碼:
分配實(shí)例類空間
也就是生成了一個(gè)InstanceKlass
Klass的描述為:
Klass的規(guī)定
在一個(gè)C++類中存在這兩個(gè)行為
所以說明,我們之前要找的字節(jié)碼解析完后就存放在這個(gè)地方
而我們知道對(duì)象有兩個(gè)部分,一個(gè)是屬性(field),一個(gè)是方法(method)
那ClassFileParser肯定是有這兩個(gè)部分的處理邏輯的,而且按照面向?qū)ο蟮乃枷?#xff0c;這兩個(gè)肯定也會(huì)分別存儲(chǔ)在某個(gè)容器中
看到這里我們分別要去看看parse_fields的產(chǎn)物和parse_methods的產(chǎn)物
根據(jù)上圖,字節(jié)碼屬性的信息存放在了FieldInfo中
按照手冊(cè)規(guī)定
parse_methods方法中的產(chǎn)物如下:
也就是將方法數(shù)據(jù)存放在了Method中:
那么除了方法和屬性之外,還得需要有個(gè)存放靜態(tài)數(shù)據(jù)的地方吧?也即常量池
解析常量池的方法是
最終可以得到如下圖:
對(duì)于單獨(dú)的一個(gè)類來講,在Java中是沒有什么太大作用的,那么就肯定是多個(gè)類,而多個(gè)類就肯定會(huì)相互關(guān)聯(lián)
上圖將會(huì)變化為下面這樣
那么類與類之間是怎么關(guān)聯(lián)在一起的呢??
在ELF中我們知道是有動(dòng)態(tài)鏈接庫和靜態(tài)鏈接庫,然后通過PLT和GOT 拼接組裝生成可執(zhí)行文件
是不是極度相似呢?
拿前面的字節(jié)碼來看
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僅僅是個(gè)占位符,此時(shí)并沒有具體的地址數(shù)據(jù),而在常量池表中#2 引用了#16和#17,同樣的,這里并沒有具體的地址數(shù)據(jù)值
那不就是和ELF中的動(dòng)態(tài)加載嗎?先用一個(gè)無用的地址先占著,之后再通過GOT把真實(shí)的值填上
所以 ConstantPool就是等同于維護(hù)了ELF中的符號(hào)表(Dynamic_Symbol)的元數(shù)據(jù)信息
在Java源碼文件結(jié)構(gòu)圖如下:
而我們可以很清楚的知道,作為一個(gè)程序員,是不是應(yīng)該考慮將Java虛擬機(jī)源碼和操作Java虛擬機(jī)的工具代碼分開編譯??
所以就是拆分出來了 hotspot文件夾和jdk文件夾
根據(jù)elf文件來理解,hotspot是屬于Java虛擬機(jī),工具可以共同操作的,因此hotspot虛擬機(jī)應(yīng)該打包成.so/.dll動(dòng)態(tài)鏈接庫文件
而jdk中的.c文件則編譯成可執(zhí)行文件
Klass 類表示整個(gè)字節(jié)碼讀入內(nèi)存中的容器信息
ConstantPool 類表示Java字節(jié)碼所操作的對(duì)象的符號(hào)信息
Method類 表示Java字節(jié)碼的方法描述信息
FieldInfo 表示Java的成員變量信息
而我們知道,在jvm中兩大板塊:執(zhí)行引擎+GC
執(zhí)行引擎如何加載Klass呢?因此需要用到類加載器
如此,得到下面這幅圖:
類加載器讀入字節(jié)碼文件,然后創(chuàng)建上面的Klass信息,傳遞給執(zhí)行引擎,最后用GC進(jìn)行垃圾回收
由于Java堆內(nèi)存的管理是由universe進(jìn)行管理
所以u(píng)niverse主要是兩個(gè)模塊:
在openJDK源碼包下的目錄層級(jí)如下:
其中:
common : 一些公共文件,比如下載源碼的shell腳本、生成make的autoconf等文件
corba : 分布式通訊接口
hotspot : Java虛擬機(jī)實(shí)現(xiàn)
jaxp : xml文件處理代碼
jaxws : ws實(shí)現(xiàn)api
jdk : 用于操作虛擬機(jī)的工具代碼,也即jdk源碼
langtools : java語言的工具實(shí)現(xiàn)的用于測試的代碼,基礎(chǔ)實(shí)現(xiàn)類等,javac,java,javap等 打包成 tools.jar
make : make編譯文件夾
nashorn : java中的js運(yùn)行實(shí)現(xiàn)
test : 測試包
而我們研究的重點(diǎn)放在hotspot虛擬機(jī)的實(shí)現(xiàn)上以及jdk虛擬機(jī)操作工具的源碼上
也就是通過jdk包中的工具來引導(dǎo)執(zhí)行引擎
如何引導(dǎo)?使用動(dòng)態(tài)鏈接庫
而動(dòng)態(tài)鏈接庫又是數(shù)據(jù)獨(dú)立,代碼共享的,所以使用工具命令可以調(diào)用到執(zhí)行引擎的main
那么執(zhí)行引擎的main方法是怎么樣的呢?由于我們只研究在linux平臺(tái)的,所以會(huì)屏蔽掉windows平臺(tái)的一些代碼,引導(dǎo)型的啟動(dòng)代碼都是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()函數(shù)進(jìn)行了一系列必要的操作,// 如libjvm.so的加載、參數(shù)解析、Classpath的 獲取和設(shè)置、系統(tǒng)屬性設(shè)置、JVM初始化等。// 調(diào)用LoadJavaVM()加載libjvm.so并初始化相關(guān)參數(shù)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); }我們需要找到動(dòng)態(tài)鏈接庫的dlopen打開文件,dlsym查找符號(hào),以及dlclose關(guān)閉 的相關(guān)處理
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;// 初始化 執(zhí)行程序InitLauncher(javaw);// 導(dǎo)出當(dāng)前執(zhí)行狀態(tài)DumpState();// 如果JLI是追蹤執(zhí)行程序if (JLI_IsTraceLauncher()) {int i;printf("Command line args:\n");for (i = 0; i < argc ; i++) {printf("argv[%d] = %s\n", i, argv[i]);}// 添加執(zhí)行指令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);// 創(chuàng)建執(zhí)行環(huán)境CreateExecutionEnvironment(&argc, &argv,jrepath, sizeof(jrepath),jvmpath, sizeof(jvmpath),jvmcfg, sizeof(jvmcfg));ifn.CreateJavaVM = 0;ifn.GetDefaultJavaVMInitArgs = 0;if (JLI_IsTraceLauncher()) {start = CounterGet();}// 加載Java虛擬機(jī)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); }詳細(xì)去看看CreateExecutionEnvironment方法是如何創(chuàng)建執(zhí)行環(huán)境的?
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*)); // 將傳入的值存放在一個(gè)新的變量中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) { // 自動(dòng)添加了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) ) { // 找到j(luò)re的路徑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,那么就需要有函數(shù)指針去操作jvm:
而這個(gè)結(jié)構(gòu)體是在加載jvm的時(shí)候使用到了:
jboolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn) {void *libjvm;JLI_TraceLauncher("JVM path is %s\n", jvmpath);// 打開動(dòng)態(tài)鏈接庫 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; }加載結(jié)束后,就開始初始化jvm了
也就是執(zhí)行JVMInit方法
OS欲啟動(dòng)java進(jìn)程,就需要將libjvm動(dòng)態(tài)鏈接庫里的數(shù)據(jù)加載出來,并且我們希望創(chuàng)建出來的Java線程要能夠可控(調(diào)整參數(shù),創(chuàng)建新的線程,不使用原來的線程,然后用新創(chuàng)建出來的線程進(jìn)行處理),因此我們會(huì)在原來的線程上使用pthread_create創(chuàng)建出新的真正用于Java的主線程
在JavaMain的實(shí)現(xiàn)體中,有如下代碼
也即需要去看InitializeJVM方法具體是如何初始化jvm的
其中的ifn->CreateJavaVM(pvm, (void **)penv, &args);則是將jvm啟動(dòng)起來了
malloc 占用c堆內(nèi)存 開辟 Java堆內(nèi)存
所以: Java堆內(nèi)內(nèi)存:Xmx參數(shù)指定的malloc開辟的jvm的自己工作空間
? Java堆外內(nèi)存:原來的c堆內(nèi)存
JVM通過傳遞給原生C一個(gè)錨點(diǎn),通過該錨點(diǎn)向堆內(nèi)存中讀寫數(shù)據(jù),并調(diào)用libjvm中的函數(shù)指針,進(jìn)而不需要再次通過dlsym進(jìn)行符號(hào)解析
hotspot想要加載字節(jié)碼,就需要用到類加載器,而類加載器又是需要通過hotspot進(jìn)行加載的,所以告訴我們需要引入一個(gè)東西來幫忙加載類加載器,而這個(gè)東西就是類庫
由于任何一門高級(jí)語言都會(huì)有自己的庫函數(shù)以簡化開發(fā),同理,Java語言也應(yīng)有自己的庫函數(shù),而對(duì)于Java語言來講,其自己本身就是一個(gè)庫函數(shù)
而對(duì)于Java來講,所有的類都需要類加載器進(jìn)行加載,上面說了Java本身是一個(gè)庫函數(shù)也即類庫,那么Java語言本身也就該通過類加載器進(jìn)行加載
那么如果只有一個(gè)類加載器的話,我可以通過這種方式進(jìn)行操作
因此我們需要有一種保護(hù)機(jī)制,來使得用戶的應(yīng)用中寫的庫函數(shù)不會(huì)將原來的系統(tǒng)庫函數(shù)替換掉
于是為了解決這個(gè)問題,需要有多個(gè)類加載器,在Java中的類加載器使用了層級(jí)結(jié)構(gòu)
No.1只加載系統(tǒng)庫函數(shù),而應(yīng)用程序只能通過No.3的類加載器去加載,這樣就防止了應(yīng)用程序?qū)⑾到y(tǒng)庫函數(shù)覆蓋
也就是只需要讓父類加載器永遠(yuǎn)優(yōu)先于子類加載器加載即可
當(dāng)我創(chuàng)建多個(gè)業(yè)務(wù)類的類加載器,只需要共享同一個(gè)頂級(jí)的父類加載器
將類的加載交由父類優(yōu)先去加載,如果父類無法加載再向下傳遞,這便是雙親委派機(jī)制。
每一個(gè)類加載器都是通過哈希表的形式來映射類的,如果當(dāng)類一樣,方法名也一樣的時(shí)候,如何加載?建立多個(gè)類加載器啊
而這個(gè)key-value的格式就是 SystemDictionary 其中 key就是代表類加載器,value就是應(yīng)該要加載的類
對(duì)于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存儲(chǔ)了所有加載的類和映射信息
[類名,類加載器] -> class 也即 [Symbol ,oop] -> Klass**
使用以上的兩種方式: 層級(jí)結(jié)構(gòu)保證了安全性,哈希表結(jié)構(gòu)保證了隔離性
而上面的這一套機(jī)制需要有一個(gè)東西來承載實(shí)現(xiàn),也就是要通過類庫來加載這個(gè)機(jī)制
因此我們可以在JavaMain方法中找到
兩大核心文件jvm.cpp和jni.cpp的差異:
jni.cpp (Java Native Interface)用于在Java層面調(diào)C語言,也就是Java與C/C++語言溝通的橋梁
jvm.cpp java內(nèi)部的功能函數(shù),可以通過符號(hào)調(diào)用 內(nèi)建核心函數(shù)
所以我們?cè)诳?findBootClass函數(shù)的時(shí)候,也就是要找 JVM_FindClassFromBootLoader的函數(shù)調(diào)用,所以需要在jvm.cpp中查找
據(jù)以上代碼我們可以看到,并沒有BootLoader對(duì)象
也就是說,在于hotspot中的BootLoader實(shí)際上是不存在的,這是一個(gè)理論上的類加載器,而在c++層面想要加載程序并不一定非得要用到加載器類,而是可以通過c++代碼的方式加載類
jvm是c++寫的,那我直接在類庫中寫c++代碼加載的Java類算不算ClassLoader?因此無法通過Java層面getBootLoader,也即BootLoader類不管是在Java層面還在C++層面都沒有這個(gè)類的實(shí)現(xiàn)
至此,我們加載了Java中的LauncherHelper這個(gè)類
而我們知道c++是如何創(chuàng)建一個(gè)類的,以及Java創(chuàng)建類的過程
所以我們可以推斷Java對(duì)象的創(chuàng)建可以與c++類比
在c++的類代碼中,static是當(dāng)前文件共享,而Java的static是在類的空間中,static修飾的不屬于對(duì)象,而是所有對(duì)象共享
上圖中,Java由于靜態(tài)變量隸屬于類,所以需要類的構(gòu)造器,而成員變量屬于對(duì)象,則需要對(duì)象構(gòu)造器
我們?cè)趯慗ava的時(shí)候經(jīng)常會(huì)遇到static{}在大括號(hào)中賦值的寫法(靜態(tài)代碼塊),這就是Java的類構(gòu)造器,而平常寫的構(gòu)造函數(shù)則是對(duì)象的構(gòu)造器
因此:類構(gòu)造器打包static{}和靜態(tài)變量賦值語句,按順序排列 -> 由JVM對(duì)clinit類構(gòu)造器的定義
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"通過對(duì)字節(jié)碼的分析,發(fā)現(xiàn)并沒有clinit方法,因此可以推測clinit方法是在Java虛擬機(jī)中實(shí)現(xiàn)并執(zhí)行的
在前面的hotspot中看到了加載了一個(gè)LauncherHelper類,這個(gè)Java類中沒有靜態(tài)代碼塊,并且也沒有這個(gè)類的main方法
如何執(zhí)行的這個(gè)類呢?
也就是執(zhí)行了LaucherHelper中的checkAndLoadMain方法
在JavaMain中先執(zhí)行了LoadMainClass,也就是加載并調(diào)用了LauncherHelper中的checkAndLoadMain方法
然后再通過函數(shù)指針調(diào)main方法
執(zhí)行java的main方法
綜上所述,
LauncherHelper類作為Java層面的被加載的第一個(gè)類,提供了加載字節(jié)碼和其他功能函數(shù)
JNIEnv提供了C與JVM的交互方式
而bootLoader直接用c++代碼編寫,加載LauncherHelper,打破了第一個(gè)類加載器需要被加載的問題
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; }這個(gè)時(shí)候調(diào)用了systemClassLoader
而上面的重點(diǎn)代碼便是:
和
那么getLauncher又是如何get的呢?
private ClassLoader loader; public Launcher() {ClassLoader extcl;extcl = ExtClassLoader.getExtClassLoader();loader = AppClassLoader.getAppClassLoader(extcl);Thread.currentThread().setContextClassLoader(loader); }此時(shí)可以看到了ExtClassLoader和AppClassLoader
兩個(gè)的繼承關(guān)系如下:
ClassLoader定義了類加載器的頂層共有最基本的信息
SecureClassLoader引入安全機(jī)制,決定哪些類可以加載哪些類不可以被加載
URLClassLoader提供文件名,通過給定的路徑或約定的路徑去尋找類
ExtClassLoader擴(kuò)展類加載器
AppClassLoader具體應(yīng)用類加載器
那么我們要去研究ClassLoader的最基本的方法,也就是loadClass,因此需要通過ClassLoader這個(gè)類去看看他的方法定義。
將父類構(gòu)造器作為一個(gè)成員放入類中,默認(rèn)為SystemClassLoader
通過父類去加載,如果父類構(gòu)造器沒有則去通過BootStrap加載(C++本地方法)
如果都沒找到則讓子類去實(shí)現(xiàn)findClass
對(duì)于我們現(xiàn)在考慮某個(gè)類的加載而言,暫時(shí)無需關(guān)心安全問題,直接跳到URLClassLoader中
替換.成/,并在末尾添加.class 最終通過URLClassPath加載
而ExtClassLoader應(yīng)該為AppClassLoader的parent,不通過繼承關(guān)系獲得,而是使用設(shè)置值的方式,執(zhí)行l(wèi)oadClass的時(shí)候先讓parent去加載
同理,那么我想要自己實(shí)現(xiàn)一個(gè)類加載器如何做?
繼承 URLClassLoader或者ClassLoader,實(shí)現(xiàn)loadClass方法,將AppClassLoader作為parent,并實(shí)現(xiàn)自己的findClass方法
ExtClassLoader
AppClassLoader
而核心文件,如rt.jar包的內(nèi)容由BootstrapClassLoader(C++代碼)直接加載
這里需要注意的是在Launcher中有一個(gè)SystemClassLoader,這個(gè)也就是AppClassLoader
了解了整個(gè)加載過程,那么,main函數(shù)最終經(jīng)過這幾個(gè)步驟便完成了Java main的加載,
調(diào)用由虛擬機(jī)調(diào)用,因此我們需要去研究jvm是如何啟動(dòng)的
因?yàn)閖ni.cpp是連通Java與c/cpp的橋梁,但凡外部需要與jvm溝通就必須經(jīng)過jni
而在java.c中我們看到的只有一個(gè)LoadJavaVM方法中有一個(gè)JNI_CreatJavaVM
也就是意味著,這個(gè)時(shí)候?qū)ni中的Java虛擬機(jī)創(chuàng)建并動(dòng)態(tài)鏈接過來,在invoke查找符號(hào)的時(shí)候找到的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虛擬機(jī)是模擬了計(jì)算機(jī)的所有信息
既然是虛擬,那么肯定與OS有關(guān)聯(lián)關(guān)系,也必然與CPU 指令集相關(guān)
第一步肯定是需要操作系統(tǒng)的相關(guān)信息,所有系統(tǒng)初始化第一步必然要讀取操作系統(tǒng)的所有信息
// 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相關(guān)初始化完畢后需要處理參數(shù)信息
由于
是將系統(tǒng)的變量注入到全局的system_properties中去
這個(gè)時(shí)候是在創(chuàng)建Java虛擬機(jī)過程中,所以并沒有java的System類,所以需要有C++類來暫時(shí)承載system_properties
由這個(gè)全局變量保存整個(gè)system_property
而Java的System是如何獲取system properties的呢?
而getProperty 的Java代碼如下
然后從props里面拿數(shù)據(jù),而誰往props里面填入數(shù)據(jù)的呢?
肯定是初始化的時(shí)候填入的數(shù)據(jù)嘛
有native關(guān)鍵字修飾,那么就是jni的本地函數(shù)
而這個(gè)initProperties是線程啟動(dòng)的時(shí)候自動(dòng)調(diào)用
之前看到的Java和jni之間的交互是需要用到JNIEnv作為中介的
那么Java語言中用native修飾的方法該怎么找到對(duì)應(yīng)的C語言實(shí)現(xiàn)呢?
第1步,找到Java語言native修飾的方法所在的類名,類名.c就是對(duì)該方法的實(shí)現(xiàn)
第2步,將類所在的 包名.類名.方法名 中的 .替換成_ ,前面補(bǔ)上Java_
即可找到對(duì)應(yīng)的本地方法實(shí)現(xiàn)
比如,現(xiàn)在我需要找initProperties的C語言實(shí)現(xiàn)方法
那么就要先找到system.c,然后找到 Java_java_lang_System_initProperties
如圖:
原因是:
jvm需要做如下工作
1、dlopen打開動(dòng)態(tài)鏈接庫
2、按照規(guī)約 dlsym ,通過規(guī)約符號(hào)找到符號(hào)函數(shù)地址
3、保存鏈接庫的引用(后續(xù)可以使用)
4、調(diào)用獲取到的函數(shù)地址
那么為什么要定義這個(gè)規(guī)約呢?
如果不定義規(guī)約,你按照你的方式去找,我按照我的定義去寫,最終對(duì)應(yīng)不上,那你虛擬機(jī)也就找不到對(duì)應(yīng)的Java方法
因此我們需要回過頭來看看Java_java_lang_System_initProperties方法里面具體干了什么事
對(duì)于GetJavaProperties函數(shù)返回了一個(gè)內(nèi)部用于保存所有properties數(shù)據(jù)的結(jié)構(gòu)體
再通過JNIEnv調(diào)用Java中的put,remove,getProperty等方法,也即通過調(diào)用到了Java中的Properties對(duì)象
Java層面:在initProperties中需要傳入Properties對(duì)象
那么在執(zhí)行initProperties之前需要先存在有Properties對(duì)象,而這個(gè)對(duì)象需要通過JNIEnv調(diào)用Java的Properties對(duì)象屬性獲取
于是在c層面, 使用了(*env)->GetMethodID調(diào)用Properties,通過c代碼可以知道,調(diào)用了Properties的 put,remove,getProperty,找到對(duì)應(yīng)方法的地址
通過Java代碼可以知道Properties繼承自Hashtable,所以必定有put remove方法,而getProperty方法按照J(rèn)NIEnv寫法應(yīng)該是獲取這個(gè)方法的函數(shù)地址:
那么這個(gè)時(shí)候就需要知道,到底是誰,在哪兒將property傳入進(jìn)去了呢?
之前我們看到了在Threads::createVM方法中執(zhí)行了
方法,而我們需要思考的是,init_system_properties確實(shí)是設(shè)置了一個(gè)全局的系統(tǒng)配置,也即
那么誰調(diào)用了 _system_properties呢?
誰調(diào)用了system_properties呢?我們可以找到j(luò)vm.cpp中有對(duì)此的調(diào)用
這個(gè)for循環(huán)的調(diào)用來自于
所以也就是當(dāng)C語言調(diào)用JVM_InitProperties方法的時(shí)候會(huì)進(jìn)入到這個(gè)地方
誰調(diào)用了JVM_InitProperties方法?返回去看Java_java_lang_System_initProperties方法,這其中就調(diào)用了JVM_InitProperties方法,
回到createVM的流程中,
這段處理,都是初始化系統(tǒng)屬性
將很多系統(tǒng)的屬性插入到了Arguments中
那么下一步應(yīng)該是需要轉(zhuǎn)換一些參數(shù)解析處理了吧
如下,將前面設(shè)置好的系統(tǒng)屬性轉(zhuǎn)換成jvm的系統(tǒng)參數(shù)進(jìn)行解析
然后需要在每一個(gè)hotspot模塊初始化之前需要初始化大頁(如果頁小,則頁表項(xiàng)多,所以需要分配一部分大頁內(nèi)存)
經(jīng)過其他處理,進(jìn)行OS初始化第二階段
os::init_2方法中存在一個(gè)polling page,而前面的page內(nèi)存經(jīng)過設(shè)置,這里使用mmap進(jìn)行內(nèi)存分配
思考一個(gè)問題:如何將一系列Java線程停下來?
Java線程通過檢測某個(gè)標(biāo)志位,當(dāng)出現(xiàn)某個(gè)標(biāo)志的時(shí)候線程自己停下來即可。這個(gè)標(biāo)志位如何做呢?
設(shè)置一個(gè)不可訪問的頁,當(dāng)線程訪問到這個(gè)頁就報(bào)錯(cuò),進(jìn)而自動(dòng)停下來。
在什么時(shí)候檢測信號(hào)標(biāo)志位呢?在線程安全的時(shí)候去檢測嘛,也即 線程安全點(diǎn)(safe point)
完成init_2之后,需要再次調(diào)整參數(shù)存儲(chǔ)
進(jìn)行TLS初始化
之后會(huì)進(jìn)行一些處理,之后,createVM會(huì)執(zhí)行到此處:
_thread_list 實(shí)現(xiàn)Java線程列表,所有創(chuàng)建的Java線程均保存在這個(gè)列表中
_number_of_threads 線程列表中線程的個(gè)數(shù)
_number_of_non_daemon_threads 線程列表中非守護(hù)線程的個(gè)數(shù),當(dāng)這里面的線程個(gè)數(shù)為0時(shí),主線程結(jié)束,所有線程都結(jié)束
在堆中初始化全局?jǐn)?shù)據(jù)結(jié)構(gòu)和創(chuàng)建系統(tǒng)類(主函數(shù)代碼)
繼續(xù)上面的,createVM的過程:
這個(gè)main_thread也就是大家所理解的Java主線程
如果沒有JavaThread,如何表達(dá)當(dāng)前Java線程所處的狀態(tài)信息?因?yàn)檫@個(gè)狀態(tài)不屬于TCB,也不屬于PCB,用于表示當(dāng)前線程正在執(zhí)行虛擬機(jī)的代碼
而main_thread表示當(dāng)前線程的執(zhí)行體
所以這個(gè)就是用于承載 在jvm層面上對(duì)于Java線程對(duì)象的信息
下面大概講講JavaThread創(chuàng)建的過程
根據(jù)jni的規(guī)范,我們知道在Java中 new Thread().start()的jni應(yīng)該是在Thread.c中
而我們知道JVM_開頭的內(nèi)建核心函數(shù)是在jvm.cpp中
其中有一行代碼如下
通過函數(shù)指針執(zhí)行了thread_entry方法,而thread_entry干了什么事呢?
其中可以看到 vmSymbols::run_method_name(),說明,反向調(diào)用了run方法
而set_thread_state()方法傳入的是線程狀態(tài),因此我們需要去看看Java線程真實(shí)的狀態(tài)是哪些?
這個(gè)JavaThreadState是對(duì)整個(gè)Java線程所有的狀態(tài)
除此之外,Java在執(zhí)行字節(jié)碼的時(shí)候執(zhí)行的狀態(tài)應(yīng)該是:
繼續(xù)create_vm
初始化全局模塊的信息
jint init_globals() {HandleMark hm;management_init(); // 初始化管理模塊bytecodes_init(); // 初始化字節(jié)碼信息classLoader_init(); // 初始化類加載器codeCache_init(); // 初始化代碼緩存VM_Version_init(); // 初始化VM版本信息os_init_globals(); // 初始化OS全局信息stubRoutines_init1(); // 代碼路由第一步初始化jint status = universe_init(); // 初始化GC相關(guān)信息(依賴于codeCache_init和stubRoutines_init1和metaspace_init)if (status != JNI_OK) // 如果初始化失敗,返回狀態(tài)信息return status;interpreter_init(); // 初始化解釋器(在所有方法加載之前)invocationCounter_init(); // 初始化調(diào)用程序計(jì)數(shù)器(在所有方法加載之前)marksweep_init(); // 初始化標(biāo)記清除垃圾回收相關(guān)信息accessFlags_init(); // 初始化訪問修飾符模塊templateTable_init(); // 初始化字節(jié)碼模板表InterfaceSupport_init(); // 初始化接口支持模塊SharedRuntime::generate_stubs(); // 初始化代碼調(diào)用stubuniverse2_init(); // 對(duì)GC相關(guān)信息進(jìn)行二次初始化(依賴于codeCache_init和stubRoutines_init1)referenceProcessor_init(); // 初始化引用處理jni_handles_init(); // 初始化JNI回調(diào)模塊 #if INCLUDE_VM_STRUCTSvmStructs_init(); // vm結(jié)構(gòu)體初始化 #endif // INCLUDE_VM_STRUCTSvtableStubs_init(); // 初始化虛擬方法表模塊InlineCacheBuffer_init(); // 初始化內(nèi)聯(lián)代碼緩沖模塊compilerOracle_init(); // 初始化orcale設(shè)置的編譯器compilationPolicy_init(); // 初始化編譯策略模塊compileBroker_init(); // 初始化編譯管理器模塊VMRegImpl::set_regName();if (!universe_post_init()) { // 對(duì)universe也即gc相關(guān)進(jìn)行鉤子回調(diào)return JNI_ERR;}javaClasses_init(); // 初始化java類信息 (必須在虛擬方法表初始化之后)stubRoutines_init2(); // 對(duì)代碼路由二次初始化(注意: StubRoutines需要第二階段的初始化)// 所有由VM_Version_init和os::init_2調(diào)整的標(biāo)志都已經(jīng)設(shè)置,所以現(xiàn)在轉(zhuǎn)儲(chǔ)標(biāo)志。if (PrintFlagsFinal) { // 如果設(shè)置了PrintFlagsFinal為true,那么在這里打印所有參數(shù)信息CommandLineFlags::printFlags(tty, false);}return JNI_OK; }上面的 init_globals方法中的bytecode_init就是將Java字節(jié)碼進(jìn)行初始化
定義了203個(gè)字節(jié)碼信息
所以每個(gè)字節(jié)碼定義的長度都是32位或者64位
可以對(duì)應(yīng)著和RISC學(xué)習(xí)
每個(gè)字節(jié)碼長度一致
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};由于字節(jié)碼和RISC指令集很像,可以類比學(xué)習(xí)
那么需要思考一個(gè)問題
這個(gè)字節(jié)碼中,為什么沒有寄存器和內(nèi)存呢???
那這樣就引來了另外一個(gè)問題,Java虛擬機(jī)到底虛擬了什么?
必定是虛擬了真實(shí)計(jì)算機(jī)的虛擬環(huán)境
根據(jù)虛擬機(jī)的信息,我們知道如下信息:
字節(jié)碼根本不知道會(huì)執(zhí)行在哪個(gè)平臺(tái)的cpu上,所以寄存器無法定義,因此需要虛擬寄存器
于是,Java虛擬機(jī)使用棧來模擬寄存器來操作(ALU)即可
想要用棧來虛擬寄存器的操作,那么虛擬的寄存器是哪些呢?也就是說,哪個(gè)地方是虛擬的哪一種寄存器呢?
所以需要用到一個(gè)局部變量表來虛擬寄存器,通過局部變量表告訴虛擬機(jī)哪些寄存器在哪兒
綜上,想要Java虛擬機(jī)來模擬寄存器操作
第一步,使用局部變量表(一個(gè)數(shù)組)來模擬寄存器
第二步,使用棧來模擬對(duì)寄存器的操作
這種寫法便是c++解釋器對(duì)字節(jié)碼的解釋
一個(gè)循環(huán),對(duì)每一個(gè)字節(jié)碼進(jìn)行判斷解釋
而模板解釋器與C++解釋器不同
雖然Java與平臺(tái)無關(guān),但是執(zhí)行字節(jié)碼的主體是虛擬機(jī),所以虛擬機(jī)就與平臺(tái)有關(guān)了
也就是說jvm與CPU平臺(tái)和OS高度耦合,并且知道自己是運(yùn)行在哪個(gè)平臺(tái)上,所以可以直接使用平臺(tái)相關(guān)寄存器來執(zhí)行字節(jié)碼以提升性能
所以在templateTable.cpp中就定義了每個(gè)字節(jié)碼對(duì)應(yīng)的匯編指令操作
例如_iconst_1對(duì)應(yīng)為 iconst方法,參數(shù)為1
由于此時(shí)模板解釋器與平臺(tái)掛鉤,所以我們看 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等等信息保存到幾個(gè)數(shù)組中
回到init_globals方法中
初始化一系列counter用于之后的性能分析器
其中,在initialize方法中存在這個(gè)方法
load_zip_library();也即,加載了zip庫
那么肯定會(huì)調(diào)用動(dòng)態(tài)鏈接庫,也肯定會(huì)執(zhí)行dlopen方法
dll_load方法中執(zhí)行:
通過上面的代碼知道,除了加載了libjvm.so這個(gè)虛擬機(jī)本身動(dòng)態(tài)鏈接庫之外,還需要加載其他的動(dòng)態(tài)鏈接庫
從編譯的jdk代碼中可以知道需要引入libjava.so等各種動(dòng)態(tài)鏈接庫
繼續(xù)init_globals方法
所謂代碼緩存,就是jit在執(zhí)行的時(shí)候用于動(dòng)態(tài)生成的代碼片段
其中,stub就是一個(gè)函數(shù)指針,指向一個(gè)用來執(zhí)行的代碼段,可以是c++代碼也可以是匯編指令段,有輸入有輸出
ReservedSpace調(diào)用了 mmap映射一大段空間,都是虛擬地址,也就是有頁表但是沒有頁表項(xiàng)
initialize方法中調(diào)用了os的 預(yù)留內(nèi)存的方法
這個(gè)mmap分配的是虛擬內(nèi)存,沒有頁表項(xiàng)
回到init_globals中
執(zhí)行到stubRoutines_init1();
什么是stub在前面講過,那么一組stub就是stubRouting
StubRoutines為 被編譯后的匯編代碼段和運(yùn)行時(shí)系統(tǒng)提供入口點(diǎn)。
特定平臺(tái)的入口點(diǎn)被定義在特定的平臺(tái)的類中
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用來保存二進(jìn)制代碼
而 StubRoutines就是如下部分,那么誰給這些屬性賦值的呢?
StubGenerator_generate(&buffer, false); 這行代碼實(shí)現(xiàn)對(duì)上面屬性的賦值操作
之后的函數(shù)調(diào)用都會(huì)走到這個(gè)地方,尤其是invoke的時(shí)候
這里的函數(shù)指針其實(shí)就是一個(gè)用來存放地址的東西
以下代碼是stubGenerator_zero.cpp中
也即零匯編代碼(所謂零匯編,就是這個(gè)部分是由c++代碼實(shí)現(xiàn)而不是通過直接生成匯編指令實(shí)現(xiàn)),
因此需要使用ShouldNotCallThisStub()方法,也即將對(duì)于需要使用匯編的stub設(shè)置為不可達(dá)的地址,一旦執(zhí)行則報(bào)錯(cuò)
在init_globals方法中我們可以看到subroutines的初始化方法有兩個(gè)部分
分別是
因?yàn)榈诙糠中枰渌糠窒瘸跏蓟?#xff0c;因此將stubRoutines的初始化分為了兩個(gè)部分
回到init_globals
jint universe_init() {jint status = Universe::initialize_heap(); //初始化堆內(nèi)存if (status != JNI_OK) {return status;}Metaspace::global_initialize(); // 初始化元數(shù)據(jù)空間// Create memory for metadata. Must be after initializing heap for// DumpSharedSpaces.ClassLoaderData::init_null_class_loader_data(); //初始化類加載器的使用數(shù)據(jù)SymbolTable::create_table(); //創(chuàng)建初始化符號(hào)表StringTable::create_table(); // 創(chuàng)建初始化字符串表ClassLoader::create_package_info_table(); // 創(chuàng)建初始化類加載器的包信息表return JNI_OK; }對(duì)于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(); // 初始化內(nèi)存if (status != JNI_OK) {return status;}return JNI_OK; }所以,兩個(gè)分別管理垃圾回收和內(nèi)存分配的問題
gc_policy->initialize_all(); 方法就是如下
而GenCollectorPolicy繼承于CollectorPolicy
也就是分代GC策略繼承于GC策略
內(nèi)存整理:適用于對(duì)象小,而且對(duì)象存活相對(duì)較少
那么也就是說,整理算法怕 大對(duì)象,對(duì)象多
因此,將大對(duì)象和存活次數(shù)很多直接存放老年代,而一般這種整理算法都是采用復(fù)制算法
新生代和老年代的區(qū)別?
新生代用于存放存活較少的,空間較小的對(duì)象,便于內(nèi)存的復(fù)制整理
老年代存放空間占用大的,存活率很高的對(duì)象,允許存在碎片問題
新生代對(duì)象什么時(shí)候進(jìn)入老年代?
由于有些對(duì)象一直存在,不能讓它在新生代一直來回復(fù)制,所以需要讓它達(dá)到一定次數(shù)之后扔到老年代中,于是需要引入計(jì)數(shù)GC復(fù)制次數(shù)(對(duì)象年齡),當(dāng)年齡達(dá)到閾值時(shí)存放老年代
新生代為什么會(huì)需要有S1(from)和S2(to)兩個(gè)空間呢?
因?yàn)閷?duì)象需要復(fù)制,從剛誕生的分配區(qū)之后需要進(jìn)行復(fù)制整理,因此需要?jiǎng)澐謧€(gè)區(qū)域可以便于快速復(fù)制清除,兩者交換可以立即得到一塊新的空間
但是會(huì)浪費(fèi)內(nèi)存
因?yàn)樾律鸁o法對(duì)對(duì)象過多的情況進(jìn)行有效處理,所以對(duì)于批量生成的對(duì)象或者當(dāng)s1或者s2區(qū)域無法再分配內(nèi)存時(shí)直接將這些對(duì)象存放老年代
我們需要看MarkSweepPolicy對(duì)于initialize_generations的實(shí)現(xiàn):
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的初始化基本完畢
下面需要初始化堆內(nèi)存
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();是對(duì)于Java元數(shù)據(jù)區(qū)的信息初始化
而什么是元數(shù)據(jù)區(qū)呢?為什么要元數(shù)據(jù)區(qū)呢?
在沒有元數(shù)據(jù)區(qū)的時(shí)候,所有的Java對(duì)象,類信息,字節(jié)碼等所有信息全部放在Java堆內(nèi)存中,對(duì)于垃圾回收來講,怎么去區(qū)分是對(duì)象呢還是一般類信息呢?
這樣導(dǎo)致耦合度太高,那么就需要將Java對(duì)象和描述對(duì)象,類信息等數(shù)據(jù)分開,便于垃圾回收專注于對(duì)Java對(duì)象的處理
因此,元空間保存了 類的信息
而Java堆用于存放Java對(duì)象
由于是進(jìn)程虛擬內(nèi)存元空間使用的VirtualSpace開辟內(nèi)存,而且肯定是使用mmap進(jìn)行空間映射,不用malloc,因?yàn)閙alloc是使用berk指針開辟空間,無法將底部的空間利用起來,出現(xiàn)內(nèi)存泄漏的情況
ClassLoaderData::init_null_class_loader_data(); 是對(duì)于classloader的初始化
類加載器的數(shù)據(jù),
類加載器的對(duì)象信息
oop就是一個(gè)Java對(duì)象,因此ClassLoaderData其實(shí)就是與Java的ClassLoader綁定在一起的
而init_null_class_loader_data就是初始化bootstrap 類加載器
解釋器初始化:
那么我們看看Interpreter的hpp
可以看到解釋器的最終形態(tài)為:
CppInterpreter和TemplateInterpreter
而CC_INTERP_ONLY和NOT_CC_INTERP為宏定義,用編譯期的時(shí)候根據(jù)編譯配置選擇最終執(zhí)行的解釋器
由于我們研究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因?yàn)槲覀冄芯緾++的代碼,看繼承關(guān)系需要看Cpp
那么也就會(huì)調(diào)用CppInterpreter的initialized方法
_code 是 StubQueue
StubQueue是一堆函數(shù)組成的隊(duì)列 也就是解釋器代碼
由于hotspot是一個(gè)熱點(diǎn)編譯
當(dāng)某一個(gè)方法執(zhí)行到某一個(gè)次數(shù)的時(shí)候調(diào)用計(jì)數(shù)器(InvocationCounter)增加數(shù)值,到達(dá)閾值之后編譯,當(dāng)長時(shí)間不用的時(shí)候該值下降,下降到某個(gè)閾值的時(shí)候再解除編譯
CppInterpreter的初始化:
TemplateInterpreter:
對(duì)于StubQueue
運(yùn)行在解釋器中的一小段代碼
回到對(duì)CppInterpreter的研究
看到InterpreterGenerator需要分配對(duì)象,那么肯定會(huì)調(diào)用其構(gòu)造方法
說明對(duì)于Cpp解釋器來講,沒有任何匯編的東西
直接用c++的函數(shù)地址,強(qiáng)轉(zhuǎn)為address
而之前在generate_all中也見到過這種寫法
而address是一個(gè)
看到是u_char* 類型,但不重要,之后在使用的時(shí)候是可以直接強(qiáng)轉(zhuǎn)為某一個(gè)特定函數(shù)的函數(shù)原型指針:返回值(*函數(shù)名)(入?yún)? 匿名指針:返回值(*)(入?yún)?
如何執(zhí)行?(返回值(*)(入?yún)?)()再需要進(jìn)行address強(qiáng)轉(zhuǎn)為該函數(shù)-> ``((返回值(*)(入?yún)?)address)()`
回到CppInterpreterGenerator::generate_all()中:
memset將對(duì)應(yīng)的空間全部清零
由于Cpp為零匯編,所以我們?cè)谡{(diào)用JNI時(shí),必定直接使用了C++的代碼:dlopen、dlsym等函數(shù),直接調(diào)用,對(duì)于返回值,直接遵循C++函數(shù)調(diào)用規(guī)范,拿到返回值直接轉(zhuǎn)化
解釋器需要執(zhí)行方法:1.JNI相關(guān)方法 2.Java和字節(jié)碼的相關(guān)方法 3.同步代碼 4.同步代碼與1 2組合的
這些方法都需要入口: 入口在哪?函數(shù)指針嘛
那么就是找到對(duì)應(yīng)的函數(shù)指針入口點(diǎn)賦值即可
根據(jù)方法的類型,生成對(duì)應(yīng)解釋器的方法的入口表
于是以后在調(diào)方法時(shí)只需要 kind方法類型 和 函數(shù)的指針(方法入口)
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()
最終調(diào)了CppInterpreter的native_entry和normal_entry
而normal_entry方法最終開始循環(huán)執(zhí)行字節(jié)碼:
interpreter的生成器調(diào)用method_entry的宏定義,調(diào)用generate_method_entry方法,根據(jù)kind的類型調(diào)用C++函數(shù)的定義,把定義的函數(shù)指針放到_entry_table數(shù)組中。當(dāng)需要調(diào)用的時(shí)候直接invoke,拿到當(dāng)前方法的類型,在數(shù)組中匹配,找到對(duì)應(yīng)類型的方法,將方法包裝成Method之后執(zhí)行normal_entry執(zhí)行,然后執(zhí)行main_loop主循環(huán)執(zhí)行所有字節(jié)碼
注意,在解釋器初始化中的stub與stubroutines的不一樣
stubroutines的是一堆外部需要調(diào)用的函數(shù)的指針
而解釋器中的是函數(shù)指針都是給解釋器執(zhí)行jni和Java代碼的
然后最終回到JavaMain中:
這是調(diào)用jni代碼
所有的hotspot啟動(dòng)整理流程總結(jié):
在此感謝 《深入理解Java高并發(fā)編程》和《Tomcat源碼全解和架構(gòu)思維》的作者 黃俊 提供的學(xué)習(xí)思維和學(xué)習(xí)方式
總結(jié)
以上是生活随笔為你收集整理的Java虚拟机启动整体流程和基础学习(内容很多,不可快餐阅读),推理+源码论证的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于Oracle parallel(并行
- 下一篇: 华为荣耀手表GS3 评测怎么样