不啰嗦,直接看:

ISA指令强关联CPU平台,那么汇编语言也一定强关联CPU平台,而C语言又是对汇编最简单的抽象,也就一定会耦合不同CPU平台的代码,这也就是意味着,在写C/C++项目的时候,必须要考虑不同平台的代码编写差异,要根据不同的平台写不同的代码以满足不同平台的功能使用与相关优化。

而我们知道程序想要操作硬件就必须要经过操作系统,由操作系统给定的系统调用去进行硬件上的操作

OS内核是在进程的虚拟地址的1GB空间,作为进程的内核态来执行,也就是可以理解为一串代码,不是独立的执行的软件

也就是说,如果想要一个App能在不同的操作系统上运行,那么必须要有不同的操作系统宏处理或者文件以适配不同系统的系统调用

除了通过OS的系统调用操作计算机硬件外,还可以通过内联汇编直接操作CPU

对于C/C++开发人员来讲,会有一个很重要的问题需要去考虑:指针问题

野指针(指向不明空间/无效内存),内存泄漏(忘记释放),内存溢出等问题

为了规避这种操作指针操作带来的问题,那么就有两种解决方案:

  1. 让编译器识别指针问题,但是这个方式也带来不足之处,由于如果出现任何指针问题则编译不通过,使得语言编写相对死板,就不能使用一些特殊的写法让代码量减少

  2. 自动释放指针,也即垃圾回收机制。基于此想法,便出现了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的规定

  1. 语言等级的class对象(方法字典等等)
  2. 提供虚拟机对于对象的分派行为

在一个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主要是两个模块:

  1. 针对对象的创建:内存管理模块,空间分配
  2. 针对对象的销毁:GC模块

在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

- entries are only deleted at safepoints- readers cannot come to a safepoint while actively examiningan entry  (an entry cannot be deleted from under a reader)- entries must be fully formed before they are available to concurrentreaders (we must ensure write ordering)

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:

  1. determine if it’s a platform-dependent or independent 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启动整理流程总结:

  1. 初始化os相关
  2. 初始化universe (GC和堆内存)
  3. 初始化Stubroutines(一组函数指针,核心为 call_stub从C调用java,原理为:entry_point调用)
  4. 初始化解释器 也是一组函数指针,将其设置为Cpp解释器或模板解释器地址,根据方法类型生成不同的入口,放入函数地址
  5. 初始化java.lang的常用类
  6. 获取到待执行的Java类的main函数的id,然后通过JNIEnv的桥梁与JVM沟通,于是调用invoke函数
  7. 调用JavaCalls类 获取到类初始化时,对 方法进行连接时,根据方法类型设置 第四步设置的方法入口函数,然后调用第三步生成的call_stub调用该函数入口

在此感谢 《深入理解Java高并发编程》和《Tomcat源码全解和架构思维》的作者 黄俊 提供的学习思维和学习方式

Java虚拟机启动整体流程和基础学习(内容很多,不可快餐阅读),推理+源码论证相关推荐

  1. java计算机毕业设计计算机课程在线培训学习管理系统MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计计算机课程在线培训学习管理系统MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计计算机课程在线培训学习管理系统MyBatis+系统+LW文档+源码+调试部署 ...

  2. java计算机毕业设计计算机数字逻辑在线学习系统MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计计算机数字逻辑在线学习系统MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计计算机数字逻辑在线学习系统MyBatis+系统+LW文档+源码+调试部署 本源码技 ...

  3. FFmpeg学习之(一)阅读FFmpeg源码的工具选择

      工欲善其事,必先利其器.ffmpeg的源码非常多,为了方便查看各个函数之间的调用关系和定义等,windows下个人推荐的阅读工具是Source Insight.用过的朋友应该知道Source In ...

  4. 深入理解Java虚拟机(第3版)学习笔记——JAVA内存区域(超详细)

    深入理解Java虚拟机(第3版)学习笔记--JAVA内存区域(超详细) 运行时数据区域 程序计数器 java虚拟机栈 本地方法栈 java堆 方法区 运行时常量池 直接内存 对象的创建 对象的内存布局 ...

  5. 二、Netty服务端/客户端启动整体流程

    一.综述 Netty 的整体流程相对来说还是比较复杂的,初学者往往会被绕晕.所以这里总结了一下整体的流程,从而对 Netty 的整体服务流程有一个大致的了解.从功能上,流程可以分为服务启动.建立连接. ...

  6. Java虚拟机启动过程解析

    一.序言 当我们在编写Java应用的时候,很少会注意Java程序是如何被运行的,如何被操作系统管理和调度的.带着好奇心,探索一下Java虚拟机启动过程. 1.素材准备 从 Java源代码 . Java ...

  7. java内存修改_修改java虚拟机启动内存大小

    在运行java桌面应用程序的时候,有时候会因为jvm内存太小,从而内存溢出,程序崩溃. 可是通过修改 eclipse.ini 中的参数,来实现修改jvm的内存大小. -vmargs -Xms128M ...

  8. java毕业生设计在线多媒体学习社区的设计与实现计算机源码+系统+mysql+调试部署+lw

    java毕业生设计在线多媒体学习社区的设计与实现计算机源码+系统+mysql+调试部署+lw java毕业生设计在线多媒体学习社区的设计与实现计算机源码+系统+mysql+调试部署+lw 本源码技术栈 ...

  9. java计算机毕业设计计算机类专业考研交流学习平台MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计计算机类专业考研交流学习平台MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计计算机类专业考研交流学习平台MyBatis+系统+LW文档+源码+调试部署 本源 ...

最新文章

  1. 重定向dup2的本质
  2. 曝光原理_简单摄影之一 曝光原理
  3. 最大子序和—leetcode53
  4. 【机器学习】XGBoost集成算法——(理论+图解+python代码比较其他算法使用天池蒸汽数据)
  5. Android下常见的内存泄露 经典
  6. 开启事物_开启大门,聚焦在正确的事物上
  7. 基于Xilinx FPGA实现PCIE2.0接口
  8. 10分钟搞定kettle源码部署
  9. 适合计算机类研究生参加的比赛
  10. Java setlocale方法_Java MessageFormat setLocale()用法及代码示例
  11. python填充三角形颜色_python的pillow用ImageDraw.Draw.polygon如何填充半透明的颜色
  12. 方玲玉 网络营销_《网络营销》课程设计
  13. jquery stop()方法
  14. Morgan Fairchild Makes the Most of It With 'The Graduate'
  15. 多媒体播放器软件性能测试,视频播放性能测试及总结
  16. 一个定语修饰两个并列的名词。
  17. 使用opencv-python快速读取视频——进阶版
  18. 1.网络安全之windows系统知识
  19. java for 代表什么意思_java中的for是什么意思
  20. NBD(Network Block Device)简介及基本使用

热门文章

  1. oppo文件管理android在哪里,OPPO手机中缓存视频文件路径在哪里查看?怎么查看?
  2. 王者荣耀8月6日服务器维护,8月6日体验服停机更新公告
  3. 【课程】03 Richards方程数值解
  4. Exp8 web基础 20164323段钊阳
  5. linux脚本-z,shell脚本中的-a到-z的意思
  6. Vue-router路由转发
  7. IT人职业道德的反思
  8. 聊聊大数据平台上云这点事
  9. log file switch (archiving needed) 等待事件一例
  10. 网站不能正常访问的原因及几处处理方法