Java 代码何故要在虚拟机中运行?JVM 如何运行 Java 代码的?JVM 运行 Java 代码的效率如何?

在 JVM 中运行

Java 是一门高级语言,语法比较复杂,直接在硬件上运行不现实,所以运行前需要对其做一番转换。主流的转换思路是,设计一个面向 Java 语言特性的虚拟机,通过编译器将 Java 程序转换成该虚拟机能够识别的指令序列,也叫做 Java 字节码。这个名字的由来是因为 Java 字节码指令的操作码 (opcode) 被固定为一个字节

Java 虚拟机可以由硬件实现,但更常见的是在各个平台上提供软件实现。如此,一个程序被转换成 Java 字节码,就可以跨平台运行。虚拟机的另外一个好处就是提供了托管环境 (Managed Runtime),这个环境能够代替人工完成代码中容易出错的部分,比如内存管理和垃圾回收。此外,诸如数组越界、安全权限等动态检测,也可以由虚拟机完成

关于数组越界,C 语言和 Java 语言的处理方式不同,下面分别是 C 语言代码和 Java 语言代码打印字符串的例子,从中可以看出,for 循环的条件设置有问题,会导致数组越界的问题:

//C 语言int main(){  int i = 0;  int arr[3] = {0};  for (; i <= 3; i++)  {    arr[i] = 0;    print("Hello World\n");  }  return 0;}
public static void main(String[] args) {        int i = 0;        int[] arr = {0};        for (;i<=3;i++){            arr[i]=0;            System.out.println("Hello world.");        }    }

C 语言的版本会无限打印字符串,Java 版本就会抛出数组越界的异常。是因为 C 语言中,只要不是访问受限的内存,所有的内存空间都是可以自由访问的。所以 a[3] 会被定位到某块不属于数组的内存地址上,而这个地址正好是存储变量 i 的内存地址,所以 a[3] = 0 就相当于 i = 0, 所以就会导致代码无限循环。

数组越界在 C 语言中是一种未决行为,并没有规定数组访问越界时编译器应该如何处理,因为数组本质上就是访问一段连续内存,只要数组通过计算得到的内存地址是可用的,那么程序就不会报错。但是 JVM 中会做数组越界的检测,就会抛出异常。

JVM 运行 Java 字节码的具体过程

之后的虚拟机指标准 JDK 中的 HotSpot 虚拟机

从虚拟机视角

首先将它编译而成的 class 文件加载到 Java 虚拟机中,加载后的 Java 类会被存放于方法区 (Method Area) 中,实际运行时虚拟机会执行方法区内的代码。JVM 会在内存中划分出堆和栈来存储运行时的数据,而且栈会被细分为面向 Java 方法的 Java 方法栈和面向本地方法 (用 C++ 编写的 native 方法) 的本地方法栈,以及存放各个线程执行位置的 PC 寄存器:

java虚拟机运行nodejs java虚拟机运行java代码_JVM

在运行过程中,每当调用进入一个 Java 方法,JVM 会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。此栈帧的大小是提前计算好的,而且 JVM 不要求栈帧在内存空间里连续分布。最后,当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机都会弹出当前线程的当前栈帧,并将之舍弃

从硬件视角


Java 字节码无法直接执行,因此 JVM 需要将字节码翻译成机器码。HotSpot 的翻译过程有两种形式:解释执行,逐条将字节码翻译成机器码并执行;即时编译,将一个方法中包含的所有字节码编译成机器码后再执行:

java虚拟机运行nodejs java虚拟机运行java代码_字节码_02


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

JVM 的运行效率如何?

上文中提到的即时编译是提高 JVM 运行效率的方法之一。即时编译建立在程序符合二八定律的假设上,也就是百分之二十的代码占用了百分之八十的计算资源。对于大部分不常用的代码,无需耗费资源将其编译成机器码,而是采用解释执行的方式运行;对于仅占据小部分的热点代码,可以将其编译成机器码,提高运行速度。

HotSpot 中内置了多个即时编译器:C1、C2 和 Graal。引入多个即时编译器是为了在编译时间和生成代码的执行效率之间做取舍。C1 又叫 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。C2 又叫 Server 编译器,面向的是对峰值性能有要求的服务端程序,采用的优化手段相对复杂,因此编译时间较长,但生成代码的执行效率高。

从 Java 7 开始,虚拟机采用分层编译的方式:热点方法首先被 C1 编译,而后热点方法中的热点会进一步被 C2 编译。为了不干扰应用的正常运行,HotSpot 的即时编译是放在额外的 编译线程中进行的,虚拟机会根据 CPU 的数量设置编译线程的数目,并且按照 1:2 的比例配制给 C1、C2 编译器

说明部分

  1. JVM 会把热点代码编译成机器码,何不将所有的代码都编译成机器码?在服务端应用变更的频率不是很高,但是对运行时的性能要求高,若是等用到了再编译,岂不浪费时间?而且一下子完全编译好,不管之后多会儿使用,都是可以直接拿来就用的。
    :事实上 JVM 有考虑 AOT (ahead of compilation), AOT 能够在线下将 Java 字节码编译成机器码,主要是用来解决启动性能不好的问题。而对于长时间运行的程序,选择先下编译和即时编译都差不多,因为最多一两个小时之后,应该即时编译的代码也已经编译完了。此外,即时编译有程序的运行时信息,优化效果更好,也就是峰值性能更好
  2. Java 在虚拟机中运行是因为 Java 语法复杂直接通过硬件实现不现实,but 同样是高级语言的 C++ 呢?
    :直接运行指的是不经过任何编译,直接在硬件上跑,即便是 C++ 也是不可以的。C++ 的策略是直接编译成目标架构的机器码,Java 的策略是编译成一个虚拟架构的机器码。这个虚拟架构可以在物理上实现 (Java processor) 也可以用软件实现,也就是 JRE
  3. 何时使用 C1?何时使用 C2?如何区分热点方法?
    :JVM 会统计每个方法被调用了多少次,超过多少次就是热点方法,有个循环汇编计数器,用来编译热循环的。默认的分层编译应该是达到两千调 C1,达到 15000 调用 C2