概述
在创建Xcode项目里面我记录了创建一个Xcode项目以开始HotSpot的过程,这篇文正就正式开始我们的debug之旅。这个旅程以“大概弄明白JVM”为目标。也就是,希望等我写完这个系列的文章之后,出去和人家吹嘘关于虚拟机的东西,能蒙住九成的Java开发。
有感于之前我读的那些阅读源码的书,都是大段的贴源码,我这里也无法避免,但是我还是希望能够尽量写得清楚明白一些。
在这之前,要先填一个坑。就是在我前面的文章里面,我虽然构建了一个test image,然而在配置的时候却是使用了fastdebug。所以在使用Xcode的时候,它提示我代码被优化了,导致看不到变量的信息,所以这一次我重新构建了一个slowdebug版本:
bash configure --enable-debug --with-jvm-variants=server --enable-dtrace
make test-image CONF_NAME=macosx-x86_64-server-slowdebug
等一切bulid好,参考前文替换掉Xcode里面的可执行目标就行了。
准备代码
为了启动我们的虚拟机,我写了一个HelloWorld,只是简单的打印输出一句话:
public class HelloWorld {
public static void main(String[] args) {
System.out.print("Hello World");
}
}
采用jdk11来进行编译,对应的语言等级也是设置在jdk11.
而后在schema里面修改执行参数:
修改执行参数
点击运行,就能输出"Hello World"了。
创建过程——猜测
现在让我们来假设一下,如果我们是JVM设计师,这个JVM在启动的时候需要做些什么事情。
内存管理
第一件想到的事情就是GC。毕竟无论是面试还是面别人,提及JVM都是要提GC的。但是GC只是内存回收,与之对应的还有内存分配。所以,这一块可以统称为JVM内存管理。
JVM必然要在启动的时候初始化内存管理相关的东西
但是这一步还太粗糙了,还可以进一步想一下,要初始化什么东西。就内存管理来说,主要就是两块,一块是内存分配,一块是垃圾回收。比如说如果用CMS,那么内存是使用空闲链表方式组织的,所以估计在JVM初始化的时候要完成这些数据结构的初始化;又比如说G1垃圾回收器,是将内存分成不同的Region进行的,这个部分估计也是在JVM初始化完成的;再比如说,谈及GC就要谈及的老年代,年轻代,metaspace,也是在这个部分初始化完成的。
到这一步,其实能够想到,内存管理是和GC息息相关的。大多数情况下,GC算法决定了内存管理的方式。
环境参数
当我们想到内存管理后,很容易想到的就是,JVM怎么知道使用哪一个GC回收器?答案当然是依赖于参数。如果我们传递了GC回收器的参数——比如使用JVM选项-XX:+UseConcMarkSweepGC指定使用CMS算法——那么就用我们指定的,如果我们什么都没传,就使用默认的。
然后如果读者读过Oracle的一些文档,比如说JDK8 Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide,那么就会知道,默认GC回收器和平台有关。除此以外,比如说并行垃圾回收,或者并发垃圾回收的线程数量,其默认值也是和平台有关的。
于是我们可以确定:
JVM初始化的时候会收集平台信息,收集用户传递的参数
这个东西,我觉得称为创建运行环境(Context)或许更加合适一点。
预加载类
我想到的第三件事,是类加载。Java有一些类,比如说String这种类,并不是在你第一次使用的地方被加载的,而是无论你是否使用,它都会被预先加载好。而且,如果考虑到类的元数据是放在metaspace里面的,那么就意味着内存的初始化要先于这个类加载。
指令的汇编代码
这个就有点神秘了。我觉得一般人估计都不知道。JVM的指令,在早期版本(大概是1.4以前??)是直接使用C程序来解释执行的。后来出于性能的考虑,使用汇编编写了每一个指令执行的程序。
那么很显然,这些指令的汇编代码需要在指令执行之前被加载到内存特定的位置。很显然,这个部分应该也是在内存模块初始化完成之后进行的。
小结
经过前面的猜测,或者说思考,我们大概能领悟到一句没用的废话:
JVM的初始化过程,就是为了Java程序的执行而做好准备
所以我们在阅读源码的时候,就要搞清楚,这些源码究竟初始化了什么,为什么要这样,它会对Java程序的执行造成什么影响。
因此我不希望讨论太多的实现细节,我一直秉持的理念,在不做JVM开发的情况下,了解太多的细节实属没必要。与之比起来,理解JVM的设计更加有意义一点。
创建过程
JVM的真正创建入口,是在jni.cpp的static jint JNI_CreateJavaVM_inner(JavaVM **vm, void **penv, void *args)static jint JNI_CreateJavaVM_inner(JavaVM **vm, void **penv, void *args)
方法中。
在该方法中,调用了一个Threads::create_vm
方法。这个方法的实现在thread.cpp。基本上,这个方法就告诉了我们做了一些什么事情。
虽然我很想贴一串代码,但是这玩意儿太长了,四百行。然后我突然也觉得,所谓的规范就是拿来打破的。我建议大家打开这一段源码对照着往下读,不然的话,十秒以后你就可能右上角叉掉这个页面了。
我捋了一下这个方法的大概逻辑:
- 首先检测一下系统支不支持这个版本的JVM,毕竟JVM的运行对基本的硬件资源还是有点小要求的。最开始我以为这里check的是JDK版本和JVM的兼容性,后来发现并不是,它check的是平台和JVM的兼容性;
- 初始化一下TLS(Thread-Local-Storage),我感觉是TLS作为线程应该具备的基础能力,应该尽可能提早初始化;
- 初始化一下输入输出的stream,然后加载launcher的属性,初始化一下os模块;
- 初始化JDK版本,这个东西将影响后面的系统参数。我举例来说,不同的JDK版本的默认GC是不同的。所以JDK版本要先初始化,后面加载系统参数的时候才好决定默认值是什么;
- 初始化一下log。大家都懂的,debug用的;
- 加载系统参数,一大堆;
- 初始化Ergonomics的东西。Ergonomics是一个很玄学的名词,我不知道对应的中文翻译应该是什么。有些朋友可能知道,这玩意儿和GC息息相关,在garbage collection tuning guide里面有专门的一章讲这个东西。这个东西大概就是影响GC、堆的默认设置,还有根据设置的最大停顿时间以及吞吐量目标,搞点自适应的东西,以后我再来谈谈这个鬼东西;
- 再一次初始化os模块,这是一个很有意思的东西,后面解释,为什么要分两次来初始化。我这里可以大概说一下,在这个步骤之前的初始化,是依赖于os模块的,比如说Ergonomics那个部分就会涉及到内存页大小的东西,所以它要先初始化一下;但是呢,这就又有一个神奇的东西,就是os模块还和一些系统参数有关,系统参数又依赖于os模块。这就有意思了,所以JVM的设计者索性分成两次来初始化,先初始化一波独立的无依赖的os模块部分,等前面一大波都初始化之后,再次初始化一下。这一次初始化之后os模块就和jni版本,jdk版本很契合了;
- 初始化一下safe point机制。safe point也是一个神奇的东西,大家可以读一下JVM源码分析之安全点safepoint。我来一个简单明了的解释,不保证严谨,但是能让大家有个概念。safe point这个东西,假如你是一个幼儿园老师,现在一大堆的小屁孩(线程)在疯狂打闹,瞎jb乱搞,你想让他们停下来听你讲课,或者让他们停下来午休,于是你就大吼一生,那群熊孩子被你吓了一跳之后,就停下来了,老老实实坐到座位上,听你安排。这个时候,就是进入一个safe point了;
- 处理点什么Xrun和agent的东西,暂时可以忽略(毕竟我也还不懂这个是什么东西);
- 初始化线程的东西;
-
这个就是重头戏了,初始化全局数据结构和system class。因为它实在太硬核了,我得把这个方法写出来:
vm_init_globals
- 处理main thread的一大堆东西。就到现在为止,我们讨论的东西都还是系统层面上的东西,还没到Java应用层面上的东西。这个部分就是将两者结合起来,一个非常关键的点;
- 初始化JVM的同步机制,也就是Java的Object Monitor机制。一些同学可能看不懂,记住Java的synchronize关键字的实现,关键初始化就发生在这一步。嗯,这个初始化方法叫做
ObjectMonitor::Initialize()
- 初始化全局模块。这个方法是
init_globals()
。其实现在init.cpp里面。这个东西有多重要呢?我将这个方法做的部分事情列举一下:字节码初始化、类加载器初始化、JIT编译策略初始化、代码缓存初始化、解释器初始化...可以说,和Java代码执行的大部分事情,基本上都在这里初始化。读懂这个东西,初始化过程就读懂一半了。我的感觉就是,你可以不知道女朋友喜欢什么色号的口红,但是应该知道这个方法搞了什么。 - 创建VM线程并且等其就绪。要知道,JVM线程大概就分两类,一类是为了维系JVM自身的线程,如GC线程什么的;另一类就是我们在Java代码里面显式创建的线程。这部分就是前一类线程初始化的地方;
- 处理一些JVMTI的东西。JVMTI全称是JVM Tool Interface,大概就是提供了一些接口给程序员去搞什么调试分析JVM,等我有时间了去试试;
- 初始化java lang class。主要就是加载了Java lang包下的一些类。比如说臭名昭著的OOM异常就是在这里加载的。可以参考vmSymbols.hpp;
- 初始化Java native interface的东西,主要就是处理了一下类型问题;
- 标记JVM初始化完成,然而后面还有一大堆东西;
- 这个步骤很有意思,初始化signal dispatcher。这是将操作系统层面的signal和jdk结合起来的关键点;
- JIT编译器初始化,注意和
init_globals()
里面的JIT编译策略初始化相区别; - 预先加载一些JSR292里的类,为了避免死锁;
- 初始化模块系统,这是JDK9加入的特性;
- 一大堆的JVMTI生命周期的通知,如enter_start_phase,post_vm_start等;
- 系统初始化的最后一部分,包括system class loader, security manager之类的东西;
- 如果设置了一些诸如内存分析(mem profile)之类的东西,那么就初始化。这个部分可以理解为初始化一些和JVM profile相关的东西;
-
BiasedLocking::init()
。这个重要性不言而喻,偏向锁可以说是Java锁机制里面最重要的东西了; - 处理一点扫尾的事情,然后创建结束。
总结
我对比了一下上面的29条和我猜测的那四个部分,不得不承认,在JVM方面我还是一个小菜鸡。我只才到了其中的一点点。
不过那29条实在太多了,太长了,根本记不住,所以我再压缩一下,毕竟“太长不读”也是时代特点了。
我来一个简易版本:
- 加载环境参数,校验环境合法性;
- 初始化JVM对外暴露的一些接口,如JVMTI, Agent,然后在初始化的过程中暴露一下JVM创建到哪个步骤了;
- 初始化JVM的类加载机制,同步机制,锁机制,顺便加载一下JVM规范要求先加载的类;
- 初始化一下字节码执行的东西,如代码缓存,JIT之类的;
- 初始化内存管理相关,包括GC的东西;
- 处理杂务。如log,profile之类的东西;
我表示,能读到这里的都是真汉子。欢迎关注我的公众号:
白玉京过客
网友评论