动力节点口口相传的Java黄埔军校
JVM是如何运行Java代码的
2019-09-10 来源:动力节点



image.png

  作为一名Java程序员,你应该知道,Java代码有很多种不同的运行方式。比如说可以在开发工具中运行,可以双击执行jar文件运行,也可以在命令行中运行,甚至可以在网页中运行。当然,这些执行方式都离不开JRE,也就是Java运行时环境。实际上,JRE仅包含运行Java程序的必需组件,包括Java虚拟机以及Java核心类库等。我们Java程序员经常接触到的JDK(Java开发工具包)同样包含了JRE,并且还附带了一系列开发、诊断工具。然而,运行C++代码则无需额外的运行时。我们往往把这些代码直接编译成CPU所能理解的代码格式,也就是机器码。


  既然C++的运行方式如此成熟,那么你有没有想过,为什么Java要在虚拟机中运行呢,Java虚拟机具体又是怎样运行Java代码的呢,它的运行效率又如何呢?


  为什么Java要在虚拟机里运行?


  Java作为一门高级程序语言,它的语法非常复杂,抽象程度也很高。因此,直接在硬件上运行这种复杂的程序并不现实。所以呢,在运行Java程序之前,我们需要对其进行一番转换。


  这个转换具体是怎么操作的呢?当前的主流思路是这样子的,设计一个面向Java语言特性的虚拟机,并通过编译器将Java程序转换成该虚拟机所能识别的指令序列,也称Java字节码。这里顺便说一句,之所以这么取名,是因为Java字节码指令的操作码(opcode)被固定为一个字节。


  举例来说,下图的中间列,正是用Java写的Helloworld程序编译而成的字节码。可以看到,它与C版本的编译结果一样,都是由一个个字节组成的。


  并且,我们同样可以将其反汇编为人类可读的代码格式(如下图的最右列所示)。不同的是,Java版本的编译结果相对精简一些。这是因为Java虚拟机相对于物理机而言,抽象程度更高。


  #最左列是偏移;中间列是给虚拟机读的机器码;最右列是给人读的代码

  0x00;b20002;getstaticjava.lang.System.out

      0x03;1203;ldc"Hello,World!"

  0x05;b60004;invokevirtualjava.io.PrintStream.println

  0x08;b1;return


  Java虚拟机可以由硬件实现,但更为常见的是在各个现有平台(如Windows、Linux)上提供软件实现。这么做的意义在于,一旦一个程序被转换成Java字节码,那么它便可以在不同平台上的虚拟机实现里运行。这也就是我们经常说的“一次编写,到处运行”。


  虚拟机的另外一个好处是它带来了一个托管环境(ManagedRuntime)。这个托管环境能够代替我们处理一些代码中冗长而且容易出错的部分。其中最广为人知的当属自动内存管理与垃圾回收,这部分内容甚至催生了一波垃圾回收调优的业务。


  除此之外,托管环境还提供了诸如数组越界、动态类型、安全权限等等的动态检测,使我们免于书写这些无关业务逻辑的代码。


  Java虚拟机具体是怎样运行Java字节码的?


  下面以标准JDK中的HotSpot虚拟机为例,从虚拟机以及底层硬件两个角度,大概讲一讲Java虚拟机具体是怎么运行Java字节码的。


  从虚拟机视角来看,执行Java代码首先需要将它编译而成的class文件加载到Java虚拟机中。加载后的Java类会被存放于方法区(MethodArea)中。实际运行时,虚拟机会执行方法区内的代码。


  Java虚拟机在内存中划分出堆和栈来存储运行时数据,虚拟机会将栈细分为面向Java方法的Java方法栈,面向本地方法(用C++写的native方法)的本地方法栈,以及存放各个线程执行位置的PC寄存器。

image.png

  在运行过程中,每当调用进入一个Java方法,Java虚拟机会在当前线程的Java方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且Java虚拟机不要求栈帧在内存空间里连续分布。


  当退出当前执行的方法时,不管是正常返回还是异常返回,Java虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。


  从硬件视角来看,Java字节码无法直接执行。因此,Java虚拟机需要将字节码翻译成机器码。


  在HotSpot里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Timecompilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。

image.png

  前者的优势在于无需等待编译,而后者的优势在于实际运行速度更快。HotSpot默认采用混合模式,综合了解释执行和即时编译两者的优点。它会先解释执行字节码,而后将其中反复执行的热点代码,以方法为单位进行即时编译。


  Java虚拟机的运行效率究竟是怎样的?


  HotSpot采用了多种技术来提升启动性能以及峰值性能,刚刚提到的即时编译便是其中最重要的技术之一。


  即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占据了百分之八十的计算资源。


  对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。


  理论上讲,即时编译后的Java程序的执行效率,是可能超过C++程序的。这是因为与静态编译相比,即时编译拥有程序的运行时信息,并且能够根据这个信息做出相应的优化。


  举个例子,我们知道虚方法是用来实现面向对象语言多态性的。对于一个虚方法调用,尽管它有很多个目标方法,但在实际运行过程中它可能只调用其中的一个。


  这个信息便可以被即时编译器所利用,来规避虚方法调用的开销,从而达到比静态编译的C++程序更高的性能。


  为了满足不同用户场景的需要,HotSpot内置了多个即时编译器:C1、C2和Graal。Graal是Java10正式引入的实验性即时编译器,之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。C1又叫做Client编译器,面向的是对启动性能有要求的客户端GUI程序,采用的优化手段相对简单,因此编译时间较短。


  C2又叫做Server编译器,面向的是对峰值性能有要求的服务器端程序,采用的优化手段相对复杂,因此编译时间较长,但同时生成代码的执行效率较高。


  从Java7开始,HotSpot默认采用分层编译的方式:热点方法首先会被C1编译,而后热点方法中的热点会进一步被C2编译。


  为了不干扰应用的正常运行,HotSpot的即时编译是放在额外的编译线程中进行的。HotSpot会根据CPU的数量设置编译线程的数目,并且按1:2的比例配置给C1及C2编译器。


  在计算资源充足的情况下,字节码的解释执行和即时编译可同时进行。编译完成后的机器码会在下次调用该方法时启用,以替换原本的解释执行。


  总结


  Java代码之所以要在虚拟机中运行,是因为它提供了可移植性。一旦Java代码被编译为Java字节码,便可以在不同平台上的Java虚拟机实现上运行。此外,虚拟机还提供了一个代码托管的环境,代替我们处理部分冗长而且容易出错的事务,例如内存管理。


  Java虚拟机将运行时内存区域划分为五个部分,分别为方法区、堆、PC寄存器、Java方法栈和本地方法栈。Java程序编译而成的class文件,需要先加载至方法区中,方能在Java虚拟机中运行。


  为了提高运行效率,标准JDK中的HotSpot虚拟机采用的是一种混合执行的策略。


  它会解释执行Java字节码,然后会将其中反复执行的热点代码,以方法为单位进行即时编译,翻译成机器码后直接运行在底层硬件之上。


  HotSpot装载了多个不同的即时编译器,以便在编译时间和生成代码的执行效率之间做取舍。


  以上就是动力Java培训机构小编介绍的“JVM是如何运行Java代码的”的内容,希望对大家有帮助,如有疑问,请在线咨询,有专业老师随时为你服务。

开班信息

同类文章

微信搜索“动力节点Java学院“或扫二维码

关注官方微信免费领学习资料

动力节点Java培训机构,行业口碑最好的Java培训机构。

立即抢名额