Java虚拟机 ——初见

首发于公众号: DSGtalk1989

1.1 虚拟机定义与分类

虚拟机的作用是将相关语言在不同的平台上进行执行的机器。JVMJava Virtual Machine就是在不同的平台上执行java字节码的机器,所以我们通常在Oracl上面下载java版本的时候,会去选择不同的平台版本,比如Windows 64bit Mac Os 64bit等等。

我们不希望直接通过硬解释的方式让大家记住虚拟机的定义,所以不会继续针对虚拟机定义作深入的枯燥的解释,而是希望通过后面一步一步的讲解,让大家自己对虚拟机能够有一套的自己的认知和解释。

所以我们接着往下来看分类。

当前要说分布最广的java虚拟机绝对是DalvikAndroid RuntimeART,毕竟是所有的android手机都在使用的虚拟机,5.0以前是Dalvik之后是ART

实际上上面这两个可能更多的我们认为不是纯种的java虚拟机,毕竟运行的是dex文件,dexjava class的压缩包,一个dex文件中会有多干个类。而且跟JVM不同的是上述的两个Android虚拟机是寄存器结构的,而大部分虚拟机包括JVM堆栈结构的,相比较而言,寄存器结构的指令更长,堆栈的指令更多。

那么具体的运行java bytecode的虚拟机主流的有哪些呢?

  • HotSpot VM

    Oracle/Sun JDKOpen JDK我们使用的标准的java用的虚拟机就是,我们通常所说的java的什么性能啊,java的GC分析啊,如何调优啊等等,最终都是基于这种虚拟机来讨论的,即当前最最主流的java虚拟机。

  • J9 VM

    IBM开发的一款高度模块化的JVM,但是他都是走的捆绑销售,基本只被用在的他们家自己的产品上,大致性能水平跟HotSpot VM是一个级别的。

  • JRockit

    跟上面两款一起并称三大主流JVM,性能水平在一个级别,但是自从被Oracle收购之后,就变成上面两家独大了,最终只到JDK6

1.2 为什么要用虚拟机

我们用几个问题来过这一个章节。

问:为什么CC++没有虚拟机?

问的有点唐突,我们就把提问转换成普及吧。这两位大哥是没有虚拟机的,因为他们可以直接编译成机器码,直接可以生成给机器读的一个一个字节。我们可以来看一下c++经过编译之后情况

最左列是偏移;中间列是给机器读的机器码;最右列是给人读的汇编代码
0x00:  55                    push   rbp
0x01:  48 89 e5              mov    rbp,rsp
0x04:  48 83 ec 10           sub    rsp,0x10
0x08:  48 8d 3d 3b 00 00 00  lea    rdi,[rip+0x3b] 
                                    ; 加载 "Hello, World!\n"
0x0f:  c7 45 fc 00 00 00 00  mov    DWORD PTR [rbp-0x4],0x0
0x16:  b0 00                 mov    al,0x0
0x18:  e8 0d 00 00 00        call   0x12
                                    ; 调用 printf 方法
0x1d:  31 c9                 xor    ecx,ecx
0x1f:  89 45 f8              mov    DWORD PTR [rbp-0x8],eax
0x22:  89 c8                 mov    eax,ecx
0x24:  48 83 c4 10           add    rsp,0x10
0x28:  5d                    pop    rbp
0x29:  c3                    ret

也就是说,实际上各平台的真机就是C的虚拟机。

问:java为什么要用虚拟机

我们清楚了上面一个问题的答案了之后,这个问题的答案基本就呼之欲出了。

java不能像c一样直接编译成机器码给机器阅读,相比较而言,java作为高级语言,要更加的复杂抽象,直接在硬件上进行运行有点不现实。所以我们将java源码转化成虚拟机可以看得懂的指令,即java字节码。同上上面的Hello World代码,如果用java来写的话,编译成虚拟机可以看懂的java字节码是这样的

# 最左列是偏移;中间列是给虚拟机读的机器码;最右列是给人读的代码
0x00:  b2 00 02         getstatic java.lang.System.out
0x03:  12 03            ldc "Hello, World!"
0x05:  b6 00 04         invokevirtual java.io.PrintStream.println
0x08:  b1               return

相比上面的c++而言,机器码变少了,因为java虚拟机相比物理机来说,更加的抽象,所以一般只需要更少的机器码就可以做更多的操作。

不知道大家小时候都有接触过虚拟光驱么,比如网上下载了一个游戏的光盘安装包文件,但是主机的光驱必须要读的是物理的光盘,这里我们把物理的光盘可以看成C的代码,可以直接被机器识别,而网上下载的光盘安装包文件就是java的代码,需要我们使用虚拟光驱才可以执行。

前面说到在不同平台上有不同的虚拟机,我们一般碰到的都是各平台上的软件实现,即Windows64 Mac64等等,这样一来我们只需要编译成java字节码之后就可以在所有的平台上运行了。

1.3 虚拟机如何运行java代码

粗略的来看,java代码需要经过两个过程才能被执行。分别是编译.class文件和翻译机器码。虚拟机在这个过程中做的是后者的工作,前半部分由java源码编译器完成

虚拟机会通过两种方式.class文件翻译成机器码。

  1. 第一种是解释执行

​ 这种方式就是最最普通的翻译,会将字节码一个一个的翻译成机器码并执行,无需等待,上来就直接运行。

  1. 第二种是即时编译

    这里会以一个方法为维度,将整个方法的字节码均翻译成机器码后再执行,所以相比而言,开始需要等待翻译,但是真实运行的时候会更快,其实很好理解。

解释

java虚拟机结合了上述两种方式,会针对所有的代码都走解释执行,然后过程中如果出现反复调用的方法会改用即时编译,提高效率。简单的可以理解成虚拟机内部有个计数器,会记录方法调用的次数,一旦达到阈值,就会走即时编译。我们将这些调用多次的方法或者代码段称为热点代码,而HotSpot就是因此而得名。大致的流程如下

编译流程

1.4 即时编译器的分类

  • Client Compiler ——C1编译器

​ 启动速度快,但是运行的效率没有特别的高,比较适合运行桌面有界面的客户端程序

  • Server Compiler ——C2编译器

​ 启动速度慢,但是运行效率高,性能好。对于不需要立马启动的服务端程序比较友好。

首先打开命令行,输入java -version我们可以看到当前java的版本号以及虚拟机的版本号

~ Salamanca$ java -version
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)

可以看到我们的java版本号是1.8,虚拟机是HotSpot,然后我们能看到虚拟机采用的是Server编译器,但是后面说明是mixed mode,即混合模式,用户既可以指定走C1,也可以指定走C2。我们在上一节中讲到的解释执行器和即时编译器也被称作解释器编译器

在java1.7以前,HotSpot采用的是一个解释器配上上面分类的其中一个编译器进行配合的工作模式,就是上一节中的最后一张图。

java 10开始,引入了Graal的实验性即时编译器,这个我们之后再详细讲。

1.5 分层编译

又是java 1.7的分水岭,从1.7开始,采用分层编译策略。

为什么呢?

我们上面说到即时编译器进行代码编译的时候需要占用程序运行的时间,因为他需要把整个热点代码都编译完再进行执行,如果说需要把热点代码编译成优化的更好的代码所需要的时间就更长了,同时为了能够优化的更好,可能还需要解释器去收集性能监控信息,这样就更加耽误执行的速度了。所以在1.7以前,这一块一直是可以优化的点,因此分层编译策略应运而生。

  • 第0层

    程序走解释执行,解释器并不开启性能监控

  • 第1层

    走C1编译,编译成本地代码,并进行简单可靠的优化,如果需要的话增加性能监控

  • 第2层或第二层以上

    走C2编译,依然编译成本地代码,但是会开启一些耗时较长的优化,可能会根据性能的监控情况进行一些比较激进的优化。

总结的来说,总体的优化程度和执行效率是成正比的,和耗时是成反比的。优化的越好,执行的效率就越高,但是耗时会更久;性能上来说C2>C1>解释;最先开始解释,然后执行频率较高的开始走C1,随着时间的推移,依然被高频执行的开始走C2