JVM:JVM与Java体系结构

发布于:2025-04-16 ⋅ 阅读:(36) ⋅ 点赞:(0)

一、JVM 基础概念

  1. 虚拟机可分为两类
    1. 一类是系统虚拟机,用于模拟计算机系统,常见的软件有 Virtual Box、VMware 等。它们能创建虚拟的计算机系统,就如同在我们的计算机中又安装了一台计算机
    2. 另一类是程序虚拟机,例如 JVM,它是为程序运行而设计的
  2. JVM 运行在操作系统之上,运行时操作系统会为其分配内存空间。JVM 会对分配到的内存空间进行划分,包括虚拟机栈(即线程栈)、堆、本地方法栈、元空间(JDK 8 及以后 HotSpot 虚拟机对方法区的实现)、程序计数器以及直接内存

二、Java 语言特性

  1. Java “一次编译,到处运行” 的特性可以这样理解:Java 程序在运行于 JVM 之前,会通过前端编译器 javac 编译成字节码文件(即 .class 文件)。不同的操作系统需要安装与之适配的 JVM,例如在 Windows 操作系统上需安装支持 Windows 的 JVM,在 Linux 操作系统上则要安装支持 Linux 的 JVM。只要是符合规范的 JVM,都能够运行遵循 Java 字节码规范的 .class 文件
  2. JVM 会采用不同的方式将字节码文件转化为对应操作系统的 CPU 能够理解的机器指令来执行。它既可以通过解释器逐行解释执行字节码,也可以利用即时编译器(JIT)将字节码编译成机器码后再执行。例如在 Windows 操作系统上,最终生成的是能被 Windows 环境下 CPU 理解和执行的机器指令;在 Linux 操作系统上,则生成可被 Linux 环境下 CPU 识别的机器指令。这样,同一套 Java 字节码文件就能在不同操作系统上借助对应的 JVM 实现跨平台运行

三、JVM 整体结构

  1. 虚拟机栈(线程栈):
    1. 每个 Java 程序至少会有一个主线程。JVM 会为每个线程都开辟一个独立的栈,栈的基本单位是栈帧,每个线程里的方法调用会对应一个栈帧。线程过多会导致栈空间不足从而溢出;单个线程里方法调用层次过深(即方法嵌套调用过多)会导致栈溢出,如常见的无限递归调用
    2. 栈帧主要划分为操作数栈、局部变量表、动态链接和方法返回地址等部分。局部变量表用于存储方法中的局部变量及其对应的值;操作数栈根据字节码指令从局部变量表中取值进行运算等操作
  2. 程序计数器程序计数器用于记录线程当前执行的字节码指令的地址,也就是记录线程运行的位置。当 CPU 发生线程切换时,下次轮到该线程运行时,就能依据程序计数器的值知道从哪里继续执行。程序计数器是每个线程独有的,并且独立于虚拟机栈
  3. 堆空间:堆空间用于存放对象实例和数组,是所有线程共享的内存区域。堆通常分为新生代和老年代,新生代又可细分为 Eden 区、Survivor 0 区和 Survivor 1 区。对象一般首先在 Eden 区分配,经过多次垃圾回收后仍存活的对象会进入老年代
  4. 本地方法栈:本地方法栈为使用本地方法(通常是用 C 或 C++ 编写的方法)服务,因为 Java 程序的一些底层 API 是用这些语言实现的。在早期 JVM 中,本地方法栈和虚拟机栈是分开的,但随着 JVM 的发展,有些 JVM 实现(如 HotSpot)将本地方法栈和虚拟机栈进行了融合,本地方法的栈帧也在虚拟机栈中运行
  5. 元空间:元空间是 JDK 8 及以后版本中对方法区的实现。它不占用 JVM 的堆内存空间,而是直接使用操作系统的本地内存,不过会受到 JVM 参数的管理和限制。元空间主要存储类的元数据信息,包括类信息、运行时常量池等
    1. 类信息:存储字节码文件中关于类的定义、类的属性定义、类的方法定义以及类的方法的字节码等内容
    2. 运行时常量池:它是字节码文件中常量池的运行时表示,会将字节码文件中的常量池部分加载进来,并且在程序运行过程中还能动态生成常量
    3. 静态变量:存储类的静态变量,不过在 JDK 7 及以后,静态变量从方法区移到了堆中
  6. 直接内存:直接内存并不属于 JVM 运行时数据区的一部分,它不占用 JVM 的堆内存空间,而是使用操作系统的本地内存。直接内存不受 JVM 的垃圾回收机制管理,但可以通过 Java 的 NIO(New Input/Output)相关类(如 ByteBuffer)来控制和使用。使用直接内存可以提升 IO 操作的速度,因为传统的 IO 操作需要将数据在用户空间和内核空间之间多次拷贝,而直接内存可以直接在操作系统的内存中进行操作,减少了数据拷贝的次数

四、Java 代码执行流程

  1. 源码编写:开发者依据 Java 语言的语法规则编写 Java 源文件,文件扩展名为.java。在这个源文件中,包含类、方法、变量等各种 Java 程序元素,它们共同构成了程序的逻辑和功能
  2. 词法分析:Java 编译器启动后,首先进行词法分析。它就像一个 “单词识别器”,将 Java 源文件的字符流按照词法规则切分成一个个的 “单词”,也就是词法单元(Token),比如关键字(如public、class )、标识符(变量名、类名等)、运算符(+、- )、界符({、} )等。在这个过程中,编译器会识别源文件中的代码片段,把它们分类为不同类型的词法单元,为后续的语法分析做准备
  3. 语法分析:基于词法分析生成的词法单元,语法分析器开始工作。它依据 Java 语言的语法规则,对词法单元进行组合和检查,构建出抽象语法树(AST)。抽象语法树以树状结构表示程序的语法结构,清晰地展示出代码的层次逻辑关系。例如,它能明确地表示出类的定义包含哪些成员变量和方法,方法内部又包含哪些语句等。如果源文件的语法存在错误,语法分析阶段就会抛出相应的语法错误信息
  4. 语义分析:语义分析是对抽象语法树进行进一步的检查和处理。这一阶段会验证程序的语义是否正确,包括检查变量是否被正确声明和使用、方法调用是否匹配、数据类型是否兼容等。比如,如果代码中试图将一个字符串和一个整数进行相加操作,语义分析就会检测到这种类型不匹配的错误。此外,语义分析还会进行符号表的填充,符号表用于记录程序中定义的各类符号(如变量、函数等)的相关信息,包括名称、类型、作用域等,以便后续的代码生成和编译过程使用
  5. 生成字节码文件:经过词法分析、语法分析和语义分析,且都没有发现错误后,Java 编译器会将源文件编译成字节码文件,文件扩展名为.class。字节码是一种与平台无关的中间代码,它包含了 Java 虚拟机指令集(即字节码指令)、符号表以及其他辅助信息。字节码文件中的指令并非针对特定的硬件平台,而是设计为能在任何安装了 Java 虚拟机的系统上运行,这也是 Java 语言 “一次编写,到处运行” 特性的关键所在
  6. 类加载器加载:生成的字节码文件需要被加载到 Java 虚拟机(JVM)中才能执行。JVM 的类加载器负责完成这一任务。类加载器会根据类的全限定名(包括包名和类名)查找并加载对应的字节码文件。类加载过程分为加载链接(验证、准备、解析)和初始化三个阶段。在加载阶段,类加载器会通过一定的方式获取字节码文件的二进制数据;链接阶段会对字节码进行验证(确保字节码符合 JVM 规范且没有安全问题)、准备(为类的静态变量分配内存并设置初始值)和解析(将符号引用转换为直接引用);初始化阶段则会执行类的静态代码块和对静态变量的赋值操作
  7. 字节码校验器校验:字节码被加载到 JVM 后,会经过字节码校验器的校验。字节码校验器会检查字节码是否符合 JVM 的规范,确保字节码的安全性和正确性。它会验证字节码指令的格式是否正确、类型是否匹配、操作是否合法等。如果字节码存在问题,比如试图访问越界的内存、违反访问权限等,字节码校验器会阻止其执行,从而保证 JVM 的安全稳定运行
  8. 解释器解析执行或 JIT 编译器编译执行:经过校验的字节码可以通过两种方式执行
    1. 解释器解析执行:解释器会逐行读取字节码指令,并将其翻译成对应平台的机器码指令然后执行。这种方式的优点是启动速度快,因为不需要额外的编译时间;缺点是执行效率相对较低,每次执行相同的字节码都需要重新解释
    2. JIT 编译器编译执行:即时编译器(JIT)会在运行时将频繁执行的字节码(热点代码)编译成机器码。JIT 编译器通过分析程序的运行情况,找出热点代码,然后将其编译成本地机器码并缓存起来。后续再次执行这些代码时,直接执行编译后的机器码,大大提高了执行效率。JVM 会根据程序的运行情况,动态地选择是使用解释器还是 JIT 编译器来执行字节码,以平衡启动速度和执行效率
  9. 在操作系统上运行:无论是通过解释器解析执行还是 JIT 编译器编译执行,最终的目标都是在操作系统上运行 Java 程序。JVM 作为 Java 程序和操作系统之间的桥梁,负责将 Java 程序的字节码指令转化为操作系统能够理解和执行的指令。JVM 通过与操作系统进行交互,如申请内存、进行文件操作、网络通信等,实现 Java 程序在不同操作系统平台上的运行,而开发者无需关心底层操作系统的差异,只需专注于 Java 程序的开发

五、JVM的生命周期

(1)启动阶段

JVM 的启动是通过引导类加载器(Bootstrap Class Loader)创建初始类来完成的。这一过程可细分为以下几个关键步骤:

  1. 加载启动类:引导类加载器是 JVM 中最顶层的类加载器,通常由本地代码(如 C 或 C++)实现。当启动 JVM 时,它会负责加载 Java 的核心类库,这些类库位于$JAVA_HOME/jre/lib目录下,像java.lang、java.util等基础包中的类。例如,java.lang.Object类就是由引导类加载器加载的
  2. 创建初始类:引导类加载器会加载并初始化 Java 虚拟机的初始类,其中最关键的是sun.misc.Launcher类。该类负责创建扩展类加载器(Extension Class Loader)和应用程序类加载器(Application Class Loader),并设置好类加载的层次结构
  3. 执行主类:一旦类加载器体系构建完成,JVM 会定位到包含main方法的主类,并调用该主类的main方法,从而启动 Java 程序的执行。例如,当你运行java HelloWorld命令时,JVM 会找到HelloWorld类并调用其main方法

(2)运行阶段

在启动完成后,JVM 进入运行阶段,此时它以进程的形式存在于操作系统中。运行阶段的主要特点如下:

  1. 执行 Java 程序:JVM 会按照字节码指令的顺序执行 Java 程序。在执行过程中,它会进行类加载、字节码校验、解释执行或即时编译(JIT)等操作。例如,当程序调用一个新的类时,JVM 会使用相应的类加载器加载该类,并对其字节码进行校验,确保安全性和正确性
  2. 管理内存:JVM 负责管理 Java 程序的内存,包括堆内存、栈内存、方法区等。它会自动进行垃圾回收(GC),回收不再使用的对象所占用的内存,以保证内存的有效利用。例如,当一个对象不再被引用时,垃圾回收器会在合适的时机将其回收
  3. 线程调度:JVM 会管理 Java 程序中的线程。它会根据线程的优先级和状态进行调度,确保各个线程能够合理地使用 CPU 资源。例如,高优先级的线程会优先获得 CPU 时间片

(3)退出阶段

JVM 的退出情况有多种,下面分别介绍:

  1. 程序正常结束:当 Java 程序中的所有非守护线程执行完毕,或者main方法正常返回时,JVM 会正常退出。例如,一个简单的 Java 程序,在main方法中完成所有任务后,程序会自然结束,JVM 也随之退出
  2. 异常终止:如果 Java 程序在运行过程中抛出未捕获的异常,且该异常导致程序无法继续执行,JVM 会异常终止。例如,当程序访问一个空对象的方法时,会抛出NullPointerException,如果没有对该异常进行捕获和处理,JVM 可能会终止运行
  3. 操作系统错误:当操作系统出现严重错误,如内存不足、硬件故障等,可能会导致 JVM 无法继续运行,从而退出。例如,当系统内存耗尽时,JVM 可能会因为无法分配足够的内存而终止
  4. 调用特定方法:在 Java 程序中,可以调用System.exit(int status)方法来主动终止 JVM。其中,参数status为 0 表示正常退出,非 0 表示异常退出。例如,System.exit(0)会使 JVM 正常退出
  5. JNI Invocation API 控制退出:JNI(Java Native Interface)允许 Java 代码与本地代码进行交互。通过 JNI Invocation API,本地代码可以控制 JVM 的启动和退出。本地代码可以调用相关的 JNI 函数来销毁 JVM 实例,从而实现 JVM 的退出。例如,在一些混合编程的场景中,本地代码可以在合适的时机调用 JNI 函数来关闭 JVM​​​​​​​

网站公告

今日签到

点亮在社区的每一天
去签到