java 虚拟机启动过程解析
一、序言
当我们在编写 Java 应用的时候,很少会注意 Java 程序是如何被运行的,如何被操作系统管理和调度的。带着好奇心,探索一下 Java 虚拟机启动过程。
1、素材准备
从Java源代码
、Java字节码
、Java虚拟机
、操作系统
四个角度分解启动过程。
2、源代码生成字节码
利用 Java 环境提供的可执行命令javac
将源代码编译成字节码文件,编译后的字节码文件与平台无关,可跨平台运行。注意区分javac
命令是一个独立的编译应用,源代码编译完成,进程终止。java
命令启动的虚拟机进程的编译过程是将字节码指令编译成汇编指令(二进制指令)。
3、虚拟机解析字节码
Java 字节码无法直接在操作系统上创建进程,因此需要借助已经启动的虚拟机进程来解析字节码,处理字节码有两种常见方式:解释型
和编译型
。
在命令行中每运行java
命令代表启动一个 Java 虚拟机进程,各虚拟机相互独立,通过命令行参数分别对虚拟机进程进行配置。
Java 虚拟机准备启动完毕后,便可以依次解析字节码指令,正式运行Java代码
部分。
4、操作系统管理虚拟机
操作系统通过进程管理和调度 Java 虚拟机,无法感知虚拟机间接解析 Java 字节码部分。Java 字节码通过虚拟机的抽象,完成了在操作系统上运行。
二、Java 虚拟机
当运行 Java 应用时,需要先安装 Java 环境,然而安装的 Java 环境与 Java 应用有什么关系,Java 应用是如何运行起来的,下面一探究竟。
二进制可执行程序${JAVA_HOME}/bin/java
是 C++编写经过 GCC 编译器编译后形成的,探索 Java 虚拟机的运行原理,首先需要找到相应的源码。
当在安装 Java 环境时,会看到一个src.zip
压缩文件,解压后里面launcher/java.c
文件便是可执行文件java
命令的主要源码。
(一)配置 JVM 装载环境
从操作系统加载环境变量、硬件信息等运行环境信息,为后续创建 JVM 进程做准备。
(二)命令行参数解析
装载完 JVM 环境之后,需要对启动时命令行参数进行解析,该过程通过ParseArguments方法
实现,并调用AddOption方法
将解析完成的参数保存到 JavaVMOption 中。
比如常见的 JavaVMOption 参数在此步骤解析:
JVM 调优各参数解析便是在此步骤完成的。
(三)执行 main 方法
线程栈大小确定后,通过ContinueInNewThread方法
创建新线程,并执行 JavaMain 函数,大概流程如下:
1、新建 JVM 实例
InitializeJVM 方法调用 InvocationFunctions 的 CreateJavaVM 方法,即调用 JVM.dll 函数 JNI_CreateJavaVM,新建一个 JVM 实例,该过程比较复杂。
2、加载入口类
通常在命令行中运行如下命令即指明入口类路径
3、查找 main 方法
通过 GetStaticMethodID 方法查找指定 main 方法名的静态方法。
4、执行 main 方法
通过JavaCalls::call
回调执行 main 方法。需要注意的是,这里执行 main 方法不是 Java 语言的方法,是经过虚拟机解释(或者编译)后,操作系统能够理解的二进制可执行方法。
三、解析字节码
(一)解释字节码
1、基于栈指令集
基于栈的指令集优点是虚拟机解释器是可跨平台移植的,换句话说不同平台的虚拟机解释器代码可以复用。
2、基于寄存器指令集
基于寄存器指令集的优点是执行速度相对于栈较快,原因是出栈入栈本身就涉及了大量的指令,而且栈是在内存中实现的,更底层的汇编指令性能更高。
基于寄存器指令集的缺点是虚拟机解释器是不可跨平台移植,需要针对不同平台的虚拟机做不同实现。考虑到不同平台已经使用不同的虚拟机程序,因此此过程多用户透明。
虚拟机通过解释器来翻译字节码文件中的指令比较顺其自然,可是对于服务器端高频执行的程序来说,中间的翻译过程相对耗时。解释字节码的方式适用于对启动性能要求高,并且执行频率较低的应用程序。
(二)编译字节码
最初,JVM 中的字节码是由解释器( Interpreter )完成编译的,当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会把这些代码认定为热点代码
。
为了提高热点代码的执行效率,在运行时,即时编译器(JIT,Just In Time)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。
在 HotSpot 虚拟机中,内置了两种 JIT,分别为C1 编译器
和C2 编译器
,这两个编译器的编译过程是不一样的。
1、C1 编译器
C1 编译器是一个简单快速的编译器,主要的关注点在于局部性的优化,适用于执行时间较短或对启动性能有要求的程序,也称为Client Compiler
,例如,GUI 应用对界面启动速度就有一定要求。
2、C2 编译器
C2 编译器是为长期运行的服务器端应用程序做性能调优的编译器,适用于执行时间较长或对峰值性能有要求的程序,也称为Server Compiler
,例如,服务器上长期运行的 Java 应用对稳定运行就有一定的要求。
3、分层编译
分层编译将 JVM 的执行状态分为了 5 个层次:
通常情况下,C2 的执行效率比 C1 高出 30%以上。
在 Java8 中,默认开启分层编译。如果只想开启 C2,可以关闭分层编译(-XX:-TieredCompilation
),如果只想用 C1,可以在打开分层编译的同时,使用参数:-XX:TieredStopAtLevel=1
。
通过 java -version
命令行可以查看到当前虚拟机解析字节码的方式,mixed mode
表示既有解释模式也有即是编译模式。
mixed mode
代表是默认的混合编译模式,除了这种模式外,我们还可以使用-Xint
参数强制虚拟机运行于只有解释器的编译模式下;也可以使用参数-Xcomp
强制虚拟机运行于只有 JIT 的编译模式下。
仅使用解释模式
通过命令java -Xint -version
设置仅使用解释模式,interpreted mode
表示解释模式。
仅使用编译模式
通过命令java -Xcomp -version
设置仅使用编译模式,compiled mode
表示编译模式。在编译模式下,程序启动能感觉到明显的卡顿。
四、小结
通过对 Java 虚拟机启动过程的解析,特别是即时编译
环节的理解,Java 应用运行并不慢。当应用中热点代码普遍被编译成汇编指令(二进制可执行命令)存放于内存中时,可近似达到 C 语言原生程序的运行速度。
随着算力与内存成本日渐降低,通过空间复杂度置换时间复杂度的策略显然是合理的,使用 Java 语言编写需求万千变化的应用是第一选择:既有跨平台、内存安全、框架生态丰富的优点,也在运行效率方面积极改善,这种折中选择与市场反馈保持一致。
版权声明: 本文为 InfoQ 作者【乌龟哥哥】的原创文章。
原文链接:【http://xie.infoq.cn/article/b7bc48ed9314f2387ce947d18】。文章转载请联系作者。
评论