什么是java虚拟机

        我们知道在 Windows 系统上一个软件安装包是 exe 后缀的,而这个安装包在苹果的 Mac OSX 系统上是无法安装的。类似地,Mac OSX 系统上软件安装包则是 dmg 后缀,同样无法在 Windows 系统上安装。

        为什么不同系统上的软件无法安装,这是因为操作系统底层的实现是不一样的。对于 Windows 系统来说,exe 后缀的软件代码最终编译成 Windows 系统能识别的机器码。而 Mac OSX 系统来说,dmg 后缀的软件代码最终编译成 Mac OSX 系统能识别的代码。

        系统软件无法通用是一个常见的问题。但使用过 Java 的同学都知道,Java 代码可以在服务端(Linux 系统)运行,也可以在 Windows 系统运行,但我们并没有生成多份不同的代码。所以 Java 语言是如何做到的呢?

        与其他语言不同,Java 语言并不直接将代码编译成与系统有关的机器码,而是编译成一种特定的语言规范,这种语言规范我们称之为字节码。无论 Java 程序要在 Windows 系统,还是 Mac OSX 系统,抑或是 Linux 系统,它首先都得编译成字节码文件,之后才能运行。

        但即使编译成字节码文件了,各个系统还是无法明白字节码文件的内容,这时候就需要 Java 虚拟机的帮助了。Java 虚拟机会解析字节码文件的内容,并将其翻译为各操作系统能理解的机器码。

        简单地说,对于同样一份 Java 源码文件,我们编译成字节码之后,无论是 Linux 系统还是 Windows 系统都不认识。这时候 Java 虚拟机就是一个翻译官,在 Linux 系统上翻译成 Linux 机器码给 Linux 系统听,在 Windows 系统上翻译成 Windows 机器码给 Windows 系统听。这样一来,Java 就实现了「Write Once,Run Anywhere」的伟大愿景了。

        我们有时对Java 虚拟机有一个误区,会觉得 Java 虚拟机只能运行 Java 代码。但实际上 Java 虚拟机运行的是字节码文件换句话说,如果你用 php 语言写一段代码,并自己用特定编译器能生成符合字节码规范的字节码文件,那么 Java 虚拟机也是可以运行的。

        所以虽然名字是 Java 虚拟机,但 Java 虚拟机与 Java 语言没有直接关系,它只按照 Java 虚拟机规范去读取 Class 文件,并按照规定去解析、执行字节码指令,仅此而已。

        准确地说,Java 虚拟机与字节码文件(Class文件)绑定。

        到底什么是虚拟机?其实 Java 虚拟机就是一个字节码翻译器,它将字节码文件翻译成各个系统对应的机器码,确保字节码文件能在各个系统正确运行。

那么,java代码如何从源代码到机器码的呢?

        对于 Java 语言来说,其从源代码到机器码,会发生什么呢?

        三个编译器:前端编译器、JIT 编译器和AOT编译器

前端编译器:源代码到字节码

        对于 Java 虚拟机来说,其实际输入的是字节码文件,而不是 Java 文件。那么对于 Java 语言而言,其实怎么将 Java 代码转化成字节码文件的呢?我们知道在 JDK 的安装目录里有一个 javac 工具,就是它将 Java 代码翻译成字节码,这个工具我们叫做编译器。相对于后面要讲的其他编译器,其因为处于编译的前期,因此又被成为前端编译器。

         通过 javac 编译器,我们可以很方便地将 java 源文件翻译成字节码文件。

        打开一个class 文件,我们会发现是一连串的 16 进制数据流。

​​​​​​​​​​​​​​​​​​​​​cafe babe 0000 0034 001d 0a00 0600 0f09 
0010 0011 0800 120a 0013 0014 0700 1507 
0016 0100 063c 696e 6974 3e01 0003 2829 
5601 0004 436f 6465 0100 0f4c 696e 654e 
756d 6265 7254 6162 6c65 0100 046d 6169 
6e01 0016 285b 4c6a 6176 612f 6c61 6e67 
2f53 7472 696e 673b 2956 0100 0a53 6f75 
7263 6546 696c 6501 0009 4465 6d6f 2e6a 
6176 610c 0007 0008 0700 170c 0018 0019 
0100 0b48 656c 6c6f 2057 6f72 6c64 0700 
1a0c 001b 001c 0100 0444 656d 6f01 0010 
6a61 7661 2f6c 616e 672f 4f62 6a65 6374 
0100 106a 6176 612f 6c61 6e67 2f53 7973 
7465 6d01 0003 6f75 7401 0015 4c6a 6176 
612f 696f 2f50 7269 6e74 5374 7265 616d 
3b01 0013 6a61 7661 2f69 6f2f 5072 696e 
7453 7472 6561 6d01 0007 7072 696e 746c 
6e01 0015 284c 6a61 7661 2f6c 616e 672f 
5374 7269 6e67 3b29 5600 2100 0500 0600 
0000 0000 0200 0100 0700 0800 0100 0900 
0000 1d00 0100 0100 0000 052a b700 01b1 
0000 0001 000a 0000 0006 0001 0000 0001 
0009 000b 000c 0001 0009 0000 0025 0002 
0001 0000 0009 b200 0212 03b6 0004 b100 
0000 0100 0a00 0000 0a00 0200 0000 0300 
​​​​​​​0800 0400 0100 0d00 0000 0200 0e

        我们运行 javac 命令的过程,其实就是 javac 编译器解析 Java 源代码,并生成字节码文件的过程。说白了,其实就是使用 javac 编译器把 Java 语言规范转化为字节码语言规范。javac 编译器的处理过程可以分为下面四个阶段:

        第一个阶段:词法、语法分析。在这个阶段,javac 编译器会对源代码的字符进行一次扫描,最终生成一个抽象的语法树。简单地说,在这个阶段 javac 编译器会搞懂我们的代码到底想要干嘛。就像我们分析一个句子一样,我们会对句子划分主谓宾,弄清楚这个句子要表达的意思一样。

        第二个阶段:填充符号表。我们知道类之间是会互相引用的,但在编译阶段,我们无法确定其具体的地址,所以我们会使用一个符号来替代。在这个阶段做的就是类似的事情,即对抽象的类或接口进行符号填充。等到类加载阶段,javac 编译器会将符号替换成具体的内存地址

        第三个阶段:注解处理。我们知道 Java 是支持注解的,因此在这个阶段会对注解进行分析,根据注解的作用将其还原成具体的指令集。

        第四个阶段:分析与字节码生成。到了这个阶段,javac 编译器便会根据上面几个阶段分析出来的结果,进行字节码的生成,最终输出为 class 文件。

        我们一般称 javac 编译器为前端编译器,因为其发生在整个编译的前期。常见的前端编译器有 Sun 的 javac,Eclipse JDT 的增量式编译器(ECJ)。

JIT编译器:从字节码到机器码

        当源代码转化为字节码之后,其实要运行程序,有两种选择。一种是使用 Java 解释器(java interpreter)解释执行字节码,另一种则是使用 JIT 编译器将字节码转化为本地机器代码

        这两种方式的区别在于,前者(Java 解释器)启动速度快但运行速度慢,而后者(JIT 编译器)启动速度慢但运行速度快。至于为什么会这样,其原因很简单。因为解释器不需要像 JIT 编译器一样,将所有字节码都转化为机器码,自然就少去了优化的时间。而当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。所以在实际情况中,为了运行速度以及效率,我们通常采用两者相结合的方式进行 Java 代码的编译执行。

        在 HotSpot 虚拟机内置了两个即时编译器(JIT),分别称为 Client Compiler 和Server Compiler。这两种不同的编译器衍生出两种不同的编译模式,我们分别称之为:C1 编译模式,C2 编译模式

        注意:现在许多人习惯上将 Client Compiler 称为 C1 编译器,将 Server Compiler 称为 C2 编译器,但在 Oracle 官方文档中将其描述为 compiler mode(编译模式)。所以说 C1 编译器、C2 编译器只是我们自己的习惯性称呼,并不是官方的说法。这点需要特别注意。

那么 C1 编译模式和 C2 编译模式有什么区别呢?

        C1 编译模式会将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑。而 C2 编译模式,也是将字节码编译为本地代码,但是会启用一些编译耗时较长的优化,甚至会根据性能监控信息进行一些不可靠的激进优化。

        简单地说 C1 编译模式做的优化相对比较保守,其编译速度相比 C2 较快。而 C2 编译模式会做一些激进的优化,并且会根据性能监控做针对性优化,所以其编译质量相对较好,但是耗时更长。

那么到底应该选择 C1 编译模式还是 C2 编译模式呢?(没明白)

实际上对于 HotSpot 虚拟机来说,其一共有三种运行模式可选,分别是:

  • 混合模式(Mixed Mode) 。即 C1 和 C2 两种模式混合起来使用,这是默认的运行模式。如果你想单独使用 C1 模式或 C2 模式,使用 -client 或 -server 打开即可。
  • 解释模式(Interpreted Mode)。即所有代码都解释执行,使用 -Xint 参数可以打开这个模式。
  • 编译模式(Compiled Mode)。 此模式优先采用编译,但是无法编译时也会解释执行,使用 -Xcomp 打开这种模式。

在命令行中输入 java -version 可以看到,机器上的虚拟机使用 Mixed Mode 运行模式。

AOT编译器:源代码直接到机器码

        AOT 编译器的基本思想是:在程序执行前生成 Java 方法的本地代码,以便在程序运行时直接使用本地代码。

        但是 Java 语言本身的动态特性带来了额外的复杂性,影响了 Java 程序静态编译代码的质量。例如 Java 语言中的动态类加载,因为 AOT 是在程序运行前编译的,所以无法获知这一信息,所以会导致一些问题的产生。类似的问题还有很多,这里就不一一举例了。

        总的来说,AOT 编译器从编译质量上来看,肯定比不上 JIT 编译器。其存在的目的在于避免 JIT 编译器的运行时性能消耗或内存消耗,或者避免解释程序的早期性能开销。

        在运行速度上来说,AOT 编译器编译出来的代码比 JIT 编译出来的慢,但是比解释执行的快。而编译时间上,AOT 也是一个始终的速度。所以说,AOT 编译器的存在是 JVM 牺牲质量换取性能的一种策略。就如 JVM 其运行模式中选择 Mixed 混合模式一样,使用 C1 编译模式只进行简单的优化,而 C2 编译模式则进行较为激进的优化。充分利用两种模式的优点,从而达到最优的运行效率。

        在 JVM 中有三个非常重要的编译器,它们分别是:前端编译器、JIT 编译器、AOT 编译器。

        前端编译器,最常见的就是我们的 javac 编译器,其将 Java 源代码编译为 Java 字节码文件。JIT 即时编译器,最常见的是 HotSpot 虚拟机中的 Client Compiler 和 Server Compiler,其将 Java 字节码编译为本地机器代码。而 AOT 编译器则能将源代码直接编译为本地机器码。

参考文档:

        JVM基础系列第3讲:到底什么是虚拟机? - 陈树义 - 博客园

        JVM基础系列第4讲:从源代码到机器码,发生了什么? - 陈树义 - 博客园

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值