深入理解Java虚拟机:(一)Java代码是如何运行的?

一、前言

我刚接触Java的时候,我和我同学就Java的运行环境要下载JRE还是JDK争议了好久,我认为 JREJava Runtime Environment,翻译过来就是Java运行环境,所以我当时认为Java代码要运行当然要安装JRE。而我同学强硬说要安装JDK,至于原因他也说不上来,反正写代码就要先下载个JDK,最后的聊天以他对我说的一句话结束,我到现在还清晰的记得:不要以为你英语好就觉得你说的是对的。

谁对谁错,我就不去讨论了,直到后来熟悉了,我才对 JDKJRE 有一个全新的认知了。
我们安装 JDK 成功了以后,在安装目录D:\Tools\jdk\jdk-8u91(这里是我自己的安装目录哈)下可以看到:

在这里插入图片描述
jdk 的子目录下也存在一个 jre。

而我 C盘目录下也有一个 jre。
在这里插入图片描述
这两个 jre 有啥联系吗?

答案是:没有联系。甚至准确的来说,它俩是一样的,无论是用哪一个都是可以的。只是很多人习惯将会单独安装另一个 jre,虽然单独安装的 jre 也并没有被使用,原因可能就是刚开始大家都不清楚 jdk 和 jre 之间的关系,所以就默认的都安装上了。

在 jdk 的 bin 目录下,基本上都是一些可执行文件,并且它们还不大。其实这些可执行文件只是外层的一层封装而已,这样的目的是避免输入的命令过长。例如 javac.exe 内部调用的其实是 JDK 中 lib 目录中的 tools.jar 中 com.sun.tools.javac.Main 类,也就是说这些工具只是入口而已。而实际上它们本身又都是由 Java 编写的,所以在 jdk 目录下的 jre 既提供了这些工具的运行时环境,也提供了我们编写完成的 Java 程序的运行时环境。

所以,很明显,jdk 是我们的开发工具包,它集成了 jre ,因此我们在安装 jdk 的时候可以选择不再安装 jre 而直接使用 jdk 中的 jre 运行我们的 Java 程序。(但是大部分人都默认将两个都装上了)。但是如果你的电脑不是用来开发 Java 程序的,而仅仅是用来部署和运行 Java 程序的,那么完全可以不用安装 jdk,只需要安装 jre 即可。


看完了这些我相信大家心中有了自己的答案了吧。我同学之所以这么强硬的说运行要安装jdk,是因为jdk里集成了jre;而我说运行要安装jre,那也确实要有jre。所以都没有错,谁叫当初都是新手呢,哈哈哈。

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

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

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

我们来举一个例子,下面代码的中间列,正是用Java写的HelloWorld程序编译的字节码。可以看出,和C版本的编译结果一样,都是由一个个字节组成的。

并且,我们可以将其反汇编人类可读的代码格式,如下代码的最右列。不同的是,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

1、Java有一个最大的特性就是“一次编写,到处运行”。一旦一个程序被转换成 Java 字节码,那么它便可以在不同平台上的虚拟机实现里运行。

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

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

三、运行原理

我们来写一段简单的代码,然后看java代码一步一步是如何执行的。

我们先来看一张执行原理图

在这里插入图片描述

1、代码如下

// JavaProcessTest 被public修饰,故存储该java源代码的文件名为JavaProcessTest
public class JavaProcessTest {
    public static void main(String[] args) {
        System.out.println("java execute process test.");
    }
}
// 由于JavaProcessTest被public修饰了,故class A不能用public修饰
class A{}
// 同理
class B{}

(1)、java源文件名就是该源文件中public类的名称。
(2)、一个java源文件可以包含多个类,但只允许一个类为public。

2、编译java源代码

当java源程序编码结束后,就需要编译器编译。

我们打开jdk目录,有两个.exe文件,即javac.exe(编译源代码,xxx.java文件) 和 java.exe(执行字节码,xxx.class文件)。

在这里插入图片描述
(1)、切换到 含有JavaProcessTest文件的目录下
在这里插入图片描述
(2)、javac.exe编译JavaProcessTest.java
在这里插入图片描述

(3)、编译后,发现目录多了以class为后缀的文件:A.class,B.class和JavaProcessTest.class

在这里插入图片描述
Tip:当javac.exe编译java源代码时,java源代码有几个类,就会编译成几个对应的字节码文件(.class文件)。

其中,字节码文件的文件名就是每个类的类名。需要注意的是,类即使不在源文件中定义,但被源文件引用,编译后,也会编程相应的字节码文件。

如类A引用类C,但类C不定义在类A的源文件中,编译后,类C也被编译成对应的字节码文件C.class。

(3)、执行java源文件

用java.exe执行即可。

在这里插入图片描述
居然报错了!!!

在包下的类,在Java源文件的地方编译后,需要到最外层包的上一级目录下运行,而且类前面需要带包名,以.隔开。请看下图:

在这里插入图片描述
到现在,java源程序基本执行结果,并正确打印我们期望的结果,那么,如上的步骤,我们可以总结如下:

在这里插入图片描述

如上总结,已经抽象化了在JVM中的执行。接下来,我们将分析字节码文件(.class文件)如何在虚拟机中一步一执行的。

3、JVM如何执行字节码文件

(1)、装载字节码文件

当 .java 源码被 javac.exe 编译器编译成 .class 字节码文件后,接下来的工作就交给JVM处理。

JVM首先通过类加载器(ClassLoader),将class文件和相关Java API加载装入JVM,以供JVM后续处理。

在该阶段中,涉及到如下一些基本概念和知识。

①.JDK,JRE和JVM关系

  • JDK(Java Development Kit),Java开发工具包,主要用于开发,在JDK7前,JDK包括JRE。
  • JRE(Java Runtime Environment),Java程序运行的核心环境,包括JVM和一些核心库。
  • JVM(Java Virtual Machine),VM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的,是JRE核心模块。

②.JVM

Java虚拟机的主要任务是装载class文件,并执行其中的字节码,不同的Java虚拟机中,执行引擎可能有不同的实现。

大致有如下几种引擎:

  • 一次性解释字节码引擎
  • 即时编译引擎
  • 自适应优化器

关于虚拟机的实现方式,采用软件方式、硬件方式和软件硬件结合方式,这个要根据具体厂商而定。

③.什么是ClassLoader

虚拟机的主要任务是装载class文件并执行其中的字节码,而class文件是由虚拟机的类加载器(ClassLoader)完成的,在一个Java虚拟机中有可能存在多个类加载器。

任何java运用程序,可能会使用两种类加载器,即启动类加载器(bootstrap)和用户自定义类加载器。

启动类加载器是Java虚拟机唯一实现的一部分,它又可分为原始类装载器,系统类装载器或默认类装载器。它的主要作用是从操作系统的磁盘装载相应的类,如Java API类等。

用户自定义装载类,即按照用户自定义的方式来装载类。

在这里插入图片描述

(2)、将字节码文件存储在JVM内存区

当JAVA虚拟机运行一个程序时,它需要内存来存储许多东西。

比如如字节码,程序创建的对象,传递给方法的参数,返回值,局部变量以及运算的中间结果等,这些相关信息被组织到“运行时数据区”。

根据厂商的不同,在Java虚拟机中,运行时数据区也有所不同。有些运行时数据区由线程共享,有些只能由某个特定线程共享。

运行时数据区大致可分几个区:方法区,堆区,栈区,PC寄存器区和本地方法栈区。

在该阶段中,涉及到如下基本概念和知识。

①.方法区

方法区用来存储解析被加载的class文件的相关信息。

当虚拟装载一个class文件后,它会从这个class文件包含的二进制数据中解析类型信息,然后将该相关信息存储到方法区中。

②.堆

堆是用来存储相关引用类型的,如new对象。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。

③.PC寄存器

PC寄存器主要用来存储线程。当新创建一个线程时,该线程都将得到一个自己的PC寄存器(程序计数器)以及一个java栈。

Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。

④.栈区

栈区主要用来存储值类型的,如基本数据类型。需要注意的是,String为引用类型,是存在堆中的。

Java栈是由许多栈帧组成的,一个栈帧包含一个Java方法调用的状态,当线程调用一个方法时,虚拟机压入一个新的栈帧到该线程的Java栈中,当该方法返回时,这个栈帧从Java栈中弹出。

⑤.本地方法栈

本地方法栈为虚拟机使用到Native方法服务的。

在这里插入图片描述

(3)、执行引擎与运行时数据区交互

运行时数据区为执行引擎提供了执行环境和相关数据,执行引擎通过与运行时数据区交互,从而获取执行时需要的相关信息,存储执行的中间结果等。

在这里插入图片描述
(4)、执行引擎与本地方法接口

当要执行本地方法时,执行引擎将调用本地方法接口来获取相关OS本地方法。

需要注意的是,本地方法与操作系统强耦合的。

在这里插入图片描述

(5)、JVM在具体操作系统上执行

JVM通过调用本地接口来获取本地方法,从而实现在具体的平台上执行。比如在Linux系统上执行,在Window系统上执行和在Unix系统上执行。

在这里插入图片描述
4、翻译形式

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

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

四、Java虚拟机的运行效率是怎样的?

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

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

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

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

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

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

为了满足不同用户场景的需要,HotSpot 内置了多个即时编译器:C1、C2 和 Graal。Graal 是 Java 10 正式引入的实验性即时编译器,在专栏的第四部分我会详细介绍,这里暂不做讨论。

之所以引入多个即时编译器,是为了在编译时间和生成代码的执行效率之间进行取舍。C1 又叫做 Client 编译器,面向的是对启动性能有要求的客户端 GUI 程序,采用的优化手段相对简单,因此编译时间较短。

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

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

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

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

发布了332 篇原创文章 · 获赞 198 · 访问量 12万+

猜你喜欢

转载自blog.csdn.net/riemann_/article/details/103836752