Java 虚拟机是如何工作的?&Java虚拟机整体架构
最近看了一些Java虚拟机的书和博客,对JVM的整体结构有了一些理解,记录一下。
参考的资料有:
1.《Inside the Java Virtual Machine》 作者 Bill Venners
2.《The Java® Virtual Machine Specification Java SE 11 Edition》Oracle官方文档
3.《深入理解JAVA虚拟机》 第三版,作者周志明
一个Java程序在计算机底层是如何被运行的?
在深入JAVA虚拟机之前,我们对于JAVA这门语言也要有个整体的理解,并且看看JVM在整个JAVA体系中处于什么位置。
一、Java体系
JAVA的整个体系包括四种技术:
- JAVA 编程语言,即JAVA语法
- JAVA class文件格式
- JAVA API
- JAVA 虚拟机
当我们编写并运行任何一个Java程序时,都在使用上面四种技术。编写Java程序的源码时使用Java编程语言,编译源文件会生成class文件,再然后在Java虚拟机上运行class文件。当你编程序时,你需要调用JAVA API中的方法来访问系统资源,比如I/O。
看看下图:
我们可以知道,Java 虚拟机和Java API 构成了一个平台,供所有的Java程序编译运行。
因为这个平台本身是软件实现的,因此只要不同的机器上实现了这个统一的平台,JAVA程序就可以再不同的机器上运行。(这也就是Java得跨平台特性)
Java虚拟机是整个Java体系平台无关性,安全和网络移动性的核心。
现在我们也就知道Java虚拟机在整个JAVA平台中的位置。
二、Java虚拟机整体架构
1.JVM
JVM,即Java virtual machine 时实际上可能有以下三种可能性:
- 抽象的Java虚拟机规范
- 一个具体的Java虚拟机实现
- 一个运行的Java虚拟机实例
(1)抽象的Java虚拟机规范是一个在《The Java® Virtual Machine Specification Java SE 11 Edition》详细描述的概念,上述是Java虚拟机规范11,随着Java技术的发展,Java虚拟机规范也在不断的变化,所有的规范都可以在Oracle官网免费找到。
(2)一个具体的Java虚拟机实现,是遵循上述规范,利用软件或硬件技术实现的一个具体的Java虚拟机,存在各个平台上,供Java程序运行。
(3)一个运行的Java虚拟机实例指的是上述任意的一个具体Java虚拟机实现上运行了一个Java程序,每个Java应用程序都需要开启一个Java虚拟机。在操作系统看来,一个运行的Java虚拟机实例就是一个叫做java的进程。
每一个Java应用程序都跑在一个遵循Java虚拟机规范的一个具体实现的一个运行实例中。因此,讲Java虚拟机时注意区分 “规范”,“实现"和"实例”。
2.Java虚拟机的生命周期
一个运行的Java虚拟机实例,都有一个任务:运行一个Java程序。
当一个Java应用程序开始时,一个Java虚拟机运行实例也就诞生了。当程序运行结束,这个实例也就消失了。如果你在同一台电脑上使用同一个Java虚拟机实现同时开启三个Java程序,你就会得到三个Java虚拟机实例。每个Java程序运行在它自己的Java虚拟机实例中。
一个Java虚拟机实例通过调用初始类中的main() 方法来开始运行程序,main() 方法必须是public ,static ,void, 并且接收一个字符串数组作为参数。任何一个带有这样的main() 方法的类都可以作为Java程序的启动点。
// On CD-ROM in file jvm/ex1/Echo.java
class Echo {
public static void main(String[] args) {
int len = args.length;
for (int i = 0; i < len; ++i) {
System.out.print(args[i] + " ");
}
System.out.println();
}
}
你可以使用命令:java Echo Greetings, Planet
来执行它
命令中的java 告诉操作系统,你要执行一个Java程序,真实情况下,你电脑会有一个具体的虚拟机实现,我的是HotSpot.
Echo
是一个初始类的名字,类中含有public static
返回值void
,接收String
数组作参数的 的main() 方法。 "Greetings, Planet,"
是命令行参数,会传给main() 方法中的String 数组。
因此arg[0]
将会是"Greeting ," , arg[1]
将会是 “Planet”。
main() 方法是一个应用程序的初始线程,这个线程可以触发其他的线程。
在Java虚拟机内部,线程可以分为两种:守护线程和非守护线程。一个守护线程是Java虚拟机本身所使用的,比如负责垃圾收集的线程。应用程序可以将其创造的任何线程都标记为守护线程。由main() 方法开启的初始线程是一个非守护线程。
只要有非守护线程还在执行,Java程序就还在运行,Java虚拟机实例也就还存活,当所有的非守护线程都终止了,Java虚拟机实例就会退出。
在之前Echo
的例子中,main() 方法没有创造任何其他线程,它打印出命令行参数后返回。这意味着唯一的非守护线程终止了,因此Java虚拟机实例也就退出了。
3.Java虚拟机的组成部分
JVM (Java Virtual Machine) 由三个独立的部分组成:
- 类加载子系统(Class Loader Subsystem)
- 运行时数据区(Runtime Date Areas)
- 执行引擎(Execution Engine)
(1)类加载子系统
类加载子系统负责把程序运行所需要的类加载进内存,并且保证类文件的正确可用。
当我们编译一个.java 文件时会生成.class 文件,当我们的程序需要使用这个类时,就需要类加载器将其加载进内存。一个.class文件中的详细内容,可以参考Java虚拟机规范中的class file format 章节。也可以用javap 命令反编译查看。
第一个被加载进内存的通常是包含有main() 方法的类。
类加载过程包含三个阶段:加载,链接和初始化。
每个阶段要干的事大致是:
加载
找到并导入一个类型的二进制形式数据
链接
执行验证,准备和解析(可选)操作
链接阶段又细分为三个阶段:
a. 验证:保证导入的类型的正确
b. 准备:为类变量分配内存,并且给类变量赋默认值
c. 解析:将符号引用转化为直接引用
初始化
执行Java代码,将类变量赋值为合适的初始值,按照《深入理解虚拟机》第三版,这一步的实质就是执行 方法。 方法由一个类中的静态变量赋值语句和静态代码块合并组成。
然后在Java中类加载器有层次的概念,比如双亲委派模型,类加载从上到下分为启动类加载器,扩展类加载器,和应用程序加载器。每个类加载器在收到加载一个类的请求时,都将该请求委派给自己的父加载器,如,应用程序类加载器将请求委派该扩展类加载器,扩展类加载器委派给启动类加载器。启动类加载器没有父加载器,于是启动类加载器就在自己的负责的范围尝试加载该请求的类,如果找到并且成功加载进内存,就返回该类的Class对象的引用给扩展类加载器,扩展类加载器在往下将Class 引用给应用程序类加载器。如果启动类加载器没有找到该类,那么就会告知其子加载器,让子加载器也就是扩展类加载器在自己负责的范围查找并尝试加载该类。扩展类加载器的加载行为类似于启动类加载器,找到该类就返回请求类的Class对象的引用给子类加载器,没有就让子类加载器在自己范围找。流程如下图。
-
启动类加载器的搜索范围是:rt.jar包下的类
-
扩展类加载器的搜索范围是:jre/lib/ext目录
-
应用程序类加载器搜索范围是CLASSPATH环境变量指定的目录
值得一提的是,对于Java虚拟机来说类加载器只有两种,启动类加载器和用户自定义加载器,启动类加载器是虚拟机的实现的一部分,用C或C++语言实现,其他类加载器都属于用户自定义加载器,由Java语言实现,全部继承自java.lang.ClassLoader。
(2)运行时数据区
Java将程序运行所需要的内存也作了划分:
如图所示,它们分别是:
- 堆(Heap)
- Java虚拟机栈(Java Virtual Machine Stacks)
- 方法区 (Method Area)
- PC寄存器 (PC Register)
- 本地方法栈(Native Method Stacks)
A.堆里面存放的是程序运行过程中生成的对象的实例以及数组,包括类加载过程中,为每一个类生成的java.lang.Class 对象,也放在堆区。堆内的对象不需要由程序员显示的回收,而是由垃圾收集器自动判定是否回收。
B.方法区中存放的是每一个类的结构信息,比如运行时常量池,字段和方法数据,方法和构造器的代码等,类加载过程中从二进制流中解析的类信息存放在方法区。
每一个运行的Java虚拟机实例只有一个堆和一个方法区,但是一个Java虚拟机实例中可以运行很多个线程,这个堆和方法区被Java虚拟机实例中的所有线程所共享,因此需要考虑线程同步问题。
下面是堆和方法区的图示。
C.当Java虚拟机每创建一个新的线程时,就需要一个新的PC 寄存器和一个Java虚拟机栈。
如果线程正在执行一个Java方法而不是本地方法的话,PC寄存器内指示的是虚拟机下一条将要执行的指令。一个Java虚拟机栈保存的是一个线程的方法调用链信息,一个方法调用用一个栈帧表示,栈帧内保存了局部变量表,参数,返回值和中间计算结果。当一个线程新调用了一个方法时他就会在自己的Java虚拟机栈的栈顶生成一个新的栈帧(也就是入栈),当方法执行完毕就会将栈顶的栈帧出栈并销毁。
下图是每个线程对应的运行时数据区,每一个线程都不能访问其他线程的PC寄存器和虚拟机栈。
并且下图显示了Java虚拟机中同时存在的三个线程,线程1和线程2正在执行一个Java方法,线程3正在执行一个本地方法。每个线程所属的Java虚拟机栈的栈顶在图的底部,正在执行的方法用浅灰色表示。对于执行的是Java方法的线程1和线程2,它们PC寄存器(浅灰色)保存的是下一条指令的位置。而线程3执行的是本地方法,其PC寄存器(深灰色)保存的值未定义。
D.本地方法栈存放的是本地方法的调用关系,具体的实现和Java虚拟机实现有关。
(3)执行引擎
执行引擎是Java虚拟机实现的核心。在 Java 虚拟机规范中,执行引擎的行为是根据指令集定义的。对于每条指令,规范详细描述了具体实现在执行字节码时遇到指令时应该做什么,但很少说明如何做。实现设计人员可以自由决定其虚拟机实现将如何执行字节码。他们的实现可以解释执行,也可以即时编译,还可以使用新的芯片利用硬件执行,或者使用这些的组合,或者想出一些全新的技术。
与本章开头描述的术语"Java 虚拟机"的三种含义类似,术语"执行引擎"也可用于三种含义中的任何一种:抽象规范、具体实现或运行时实例。抽象规范根据指令集定义了执行引擎的行为。具体实现可能使用多种技术,可以是软件、硬件或两者的组合。执行引擎的运行时实例是一个线程。
正在运行的 Java 应用程序的每个线程都是虚拟机执行引擎的不同实例。从其生存期开始到结束,线程要么执行字节码,要么执行本机方法。线程可以通过在芯片中以本机方式或解释等方式直接执行字节码,也可以通过实时编译和执行生成的本机代码来间接执行字节码。Java 虚拟机实现可能使用正在运行的应用程序不可见的其他线程,例如执行垃圾回收的线程,此类线程不必是实现执行引擎的"实例"。但是,属于正在运行的应用程序的所有线程都是正在运行的执行引擎。
执行引擎可以分为三部分:
解释器(Interpreter):
解释器逐行的读取并执行字节码。缺点是,当一个方法被多次调用时,每次都需要解释。
Just-In-Time
JIT 编译器克服了解释器的缺点。执行引擎首先使用解释器来执行字节代码,但是当它找到一些重复的代码时,它使用JIT编译器。 然后,JIT 编译器编译整个字节码并将其更改为本机机器代码。此本机机器代码直接用于重复的方法调用,从而提高了系统的性能。
JIT编译器由四部分组成:
中间代码生成器 - 生成中间代码
代码优化器 - 优化中间代码,以提高性能
目标代码生成器 -将中间代码转化为本地的机器代码
探查器 -找到热点代码(被多次重复执行的代码)
注意:与解释器逐行解释代码相比,JIT 编译器编译代码所需的时间更多。如果您只要运行一次程序,则最好使用解释器。
(4)垃圾收集器
垃圾回收器 (GC) 从堆(Heap)区域中收集和删除未引用的对象。它是通过销毁运行时未使用的内存来自动回收它们的过程。
垃圾回收使 Java 内存高效,因为它从堆内存中删除了未引用的对象,并为新对象腾出了可用空间。它涉及两个阶段:
- 标记:该步骤标记未被使用的对象
- 清除:该步骤将上一步标记的对象清除
有三种垃圾收集器:
- SerialGC
- ParallelGC
- Garbage First(G1) GC
三、Java代码在虚拟机上运行过程
详细得过程,可以阅读上面第3节 虚拟机组成部分
,其中对Java代码运行结合虚拟机得组成作了详细讲解!