vlambda博客
学习文章列表

一文读懂 JVM 架构解析

本文约2800字,完整阅读大概会花费你「5分钟」左右的时间

文章导读


前言

每个 Java 开发人员都知道字节码经由 JRE(Java运行时环境)执行。但他们或许不知道 JRE 其实是由 Java虚拟机(JVM)实现,JVM分析字节码,解释并执行它。作为开发人员,了解 JVM 的架构是非常重要的,因为它使我们能够编写出更高效的代码。


本文中,我们将深入了解 Java 中的 JVM 架构和 JVM 的各个组件。


首先我们介绍一下 JDK、JRE 以及 JVM 之间的关系。

Java 架构


一文读懂 JVM 架构解析


Java 架构包括上图中提到的 3 个主要组件:

  • Java 开发工具包 (JDK)

  • Java 运行时环境(JRE)

  • Java 虚拟机 (JVM)


1、Java 开发工具包 (JDK)


JDK(Java Development Kit) 是整个 Java 的核心,通常称为 JRE 的超集。它是支持 Java 应用程序和 Java 小程序开发的基础组件。


它是特定于平台的,因此每个操作系统(例如,Mac、Unix 和 Windows)都需要单独的安装程序。


JDK 包括了 Java 运行环境 JRE(Java Runtime Envirnment),一堆 Java 工具(javac/java/jdb等)和 Java 基础的类库。


2、Java 运行时环境 (JRE)


JRE (Java Runtime Environment) 是 JDK 的一部分,包含 JVM 标准实现及 Java 核心类库。JRE 是 Java 运行环境,并不是一个开发环境,所以没有包含任何开发工具(如编译器和调试器)。


3、Java 虚拟机 (JVM)


JVM(Java Virtual Machine)是整个Java 实现跨平台的最核心的部分,能够运行以 Java 语言写作的,编译完成的 class 程序。屏蔽了与具体操作系统平台相关的信息,使得 Java 程序只需生成在 Java 虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。


三者的联系如下图所示:


一文读懂 JVM 架构解析


JVM


JVM 全称 Java Virtual Machine,也就是我们耳熟能详的 Java 虚拟机。它能识别 .class 后缀的文件,并且能够解析它的指令,最终调用操作系统上的函数,完成我们想要的操作。


一般情况下,使用 C++ 开发的程序,编译成二进制文件后,就可以直接执行了,操作系统能够识别它;但是 Java 程序不一样,使用 javac 编译成 .class 文件之后,还需要使用 Java 命令去主动执行它,操作系统并不认识这些 .class 文件。


为什么不能像 C++ 一样,直接在操作系统上运行编译后的二进制文件呢?而非要搞一个处于程序与操作系统中间层的虚拟机呢?


大家都知道,Java 是一门抽象程度特别高的语言,提供了自动内存管理等一系列的特性。这些特性直接在操作系统上实现是不太可能的,所以就需要 JVM 进行一番转换。


一文读懂 JVM 架构解析


有了 JVM 这个抽象层之后,Java 就可以实现跨平台了。JVM 只需要保证能够正确执行 .class 文件,就可以运行在诸如 Linux、Windows、MacOS 等平台上了。


用一句话概括 JVM 与操作系统之间的关系:JVM 上承开发语言,下接操作系统,它的中间接口就是字节码。


Java 虚拟机和字节码存储格式是实现语言无关性的基础。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。


在 Java 中编译器将 Java 文件编译为 .class 文件,然后将 .class 文件输入到 JVM 中,JVM 执行类文件的加载和执行的操作。


JVM 架构图:


一文读懂 JVM 架构解析



JVM是如何工作的?


如上图所示,JVM 中主要有三个子系统:

  • 类加载器子系统(Class Loader Sub System)

  • 运行时数据区(Runtime Data Area)

  • 执行引擎(Execution Engine)


类加载器子系统


Java 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型,这就是虚拟机的加载机制。


一文读懂 JVM 架构解析


类加载子系统作用:

  • 类加载子系统负责从文件系统或者网络中加载 class 文件,class 文件在文件开头有特定的文件标识(0xCAFEBABE)。

  • 类加载器(Class Loader)只负责 class 文件的加载,至于它是否可以运行,则由执行引擎(Execution Engine)决定。

  • 加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射)。

  • Class 对象是存放在堆区的。


类加载过程:


类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。(验证、准备和解析又统称为连接,为了支持Java语言的运行时绑定,所以解析阶段也可以是在初始化之后进行的。以上顺序都只是说开始的顺序,实际过程中是交叉的混合式进行的,加载过程中可能就已经开始验证了)。


一文读懂 JVM 架构解析



运行时数据区


一文读懂 JVM 架构解析



运行时数据区可分为5个主要组件:

  1. 方法区(Method Area):所有的类级数据将存储在这里,包括静态变量。每个JVM只有一个方法区,它是一个共享资源;

  2. 堆区域(Heap Area):所有对象及其对应的实例变量和数组将存储在这里。每个JVM也只有一个堆区域。由于方法和堆区域共享多个线程的内存,所存储的数据不是线程安全的;

  3. 虚拟机栈区(VM Stack):对于每个线程,将创建单独的运行时虚拟机栈。对于每个方法调用,将在虚拟机栈中产生一个条目,称为栈帧。所有局部变量将在堆栈内存中创建。堆栈区域是线程安全的,因为它不共享资源。堆栈框架分为三个子元素:

    • 局部变量数组(Local Variable Array):与方法相关,涉及局部变量,并在此存储相应的值

    • 操作数栈(Operand stack):如果需要执行任何中间操作,操作数堆栈将充当运行时工作空间来执行操作

    • 帧数据(Frame Data):对应于方法的所有符号存储在此处。在任何异常的情况下,捕获的区块信息将被保持在帧数据中;

  4. 本地方法栈(Native Method Stacks):本地方法堆栈保存本地方法信息。对于每个线程,将创建一个单独的本地方法堆栈。


执行引擎


执行引擎主要用来执行 Java 生成 .class 的字节码,解析/编译成各种 cpu 所能执行的二进制指令。


简单来说,JVM 中的执行引擎充当了将高级语言翻译为机器语言的译者。


执行引擎主要具有三个用于执行 Java 类的主要组件:




解释器(interpreter):Java虚拟机启动时,会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容编译为对应平台的本地机器指令执行。

  • 解释器真正意义上所承担的角色就是一个运行时「翻译者」,将字节码文件中的内容「翻译」为对应平台的本地机器指令执行。

  • 当一条字节码指令被解释执行完成后,接着再根据 PC 寄存器中记录的下一条需要被执行的字节码指令执行解释操作。


Interpreter 的主要缺点是当多次调用同一个方法时,每次都需要新的解释,这会降低系统的性能。所以这就是 JIT 编译器将与解释器并行运行的原因。

JIT (Just In Time Compiler):即时编译器,虚拟机将字节码直接编译成和本地机器平台相关的机器语言。(把热点代码编译成机器语言,编译慢,执行快)


即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。


JIT的构成组件为:

  • 中间代码生成器(Intermediate Code Generator):生成中间代码

  • 代码优化器(Code Optimizer):负责优化上面生成的中间代码

  • 目标代码生成器(Target Code Generator):负责生成机器代码或本地代码

  • 分析器(Profiler):一个特殊组件,负责查找热点,即该方法是否被多次调用;


垃圾收集器(Garbage Collector):收集和删除未引用的对象。可以通过调用  System.gc() 触发垃圾收集,但不能保证执行。JVM 的垃圾回收对象是已创建的对象。


Java 本地方法接口(JNI):JNI将与本机方法库进行交互,并提供执行引擎所需的本机库。


本地方法库(Native Method Libraries):它是执行引擎所需的本机库的集合。