一、JVM(Java虚拟机)简述:
- 是一个抽象的计算模型。
- 如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域。
- 目的是为构建在其上运行的应用程序提供一个运行环境,能够运行 java 字节码。
- JVM 可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构。

1.1、三者的协作关系
通过一个完整的 Java 程序执行流程说明:
步骤 1:类加载
-
类装载器 加载 .class 文件到 方法区。
-
类的静态变量初始化后存储在 堆 中。
步骤 2:创建线程
- JVM 为线程分配 虚拟机栈 和 程序计数器。
步骤 3:执行字节码
-
执行引擎 从方法区读取字节码指令。
-
操作 虚拟机栈 的栈帧(如压入局部变量、执行算术操作)。
-
在 堆 中创建对象实例,并操作对象数据。
步骤 4:方法调用
-
每次方法调用生成新的栈帧。
-
执行引擎通过 动态链接 访问方法区中的符号引用,解析为直接引用。
步骤 5:垃圾回收
- 执行引擎 触发垃圾回收器(GC)扫描 堆,回收不再使用的对象。
步骤 6:本地方法调用
- 调用 native 方法时,执行引擎通过 本地方法栈 与操作系统交互。
一、类装载子系统
JVM 的类装载子系统(Class Loading Subsystem是 Java 虚拟机的重要组成部分,负责加载、链接、初始化 Java 类。它的主要任务是将字节码文件(.class 文件)加载到 JVM 的元空间中,并将其转换为 JVM 可以使用的运行时数据结构。
1.1、 类装载子系统的功能
类装载子系统的主要功能包括:
- 加载(Loading):将classpath、jar包、网络、硬盘位置下的class二进制字节流读进内存中,在内存中生成一个代表这个类Java.lang.Class的对象放进元空间。
- 通过类的全限定名查找字节码文件。
- 将字节码文件加载到内存中,并生成一个 java.lang.Class 对象。
- 链接(Linking):将加载的类与 JVM 运行时状态连接起来,包括验证、准备和解析以下三个步骤:
- 验证(Verification):
- 确保字节码文件的正确性和安全性。
- 检查文件格式、语义、符号引用等。
- 准备(Preparation):
- 为类的静态变量分配内存并设置默认值(如 0、null 等)。
- 不会执行静态代码块或赋值操作。
- 解析(Resolution):
- 将符号引用转换为直接引用。
- 例如,将方法名解析为方法在内存中的地址。
- 验证(Verification):
- 初始化(Initialization):
- 执行类的静态初始化代码(如静态变量赋值和静态代码块)。
- 初始化阶段是类加载的最后一步,只有在类首次主动使用时才会触发。
- 使用(using):
- 卸载(uninstall):
1.2、类装载子系统的组成
类装载子系统由以下三个主要组件组成:
- 类加载器(Class Loader)
-
类加载器负责加载字节码文件到 JVM 中。JVM 提供了三种内置的类加载器,并支持自定义类加载器。
-
类加载器的层次结构
启动类加载器(Bootstrap Class Loader):负责加载 JVM 核心类库(如 java.lang.*),通常位于 jre/lib 目录下。由 C/C++ 实现,是 JVM 的一部分。 -
扩展类加载器(Extension Class Loader):
负责加载扩展类库(如 javax.*),通常位于 jre/lib/ext 目录下。由 Java 实现,是 sun.misc.Launcher$ExtClassLoader 的实例。 -
应用程序类加载器(Application Class Loader):
负责加载用户类路径(Classpath)上的类。由 Java 实现,是 sun.misc.Launcher$AppClassLoader 的实例。 -
自定义类加载器
用户可以通过继承 java.lang.ClassLoader 类实现自定义类加载器。
常用于热部署、模块化加载等场景。 -
双亲委派模型(Parent Delegation Model)
类加载器在加载类时,首先委托父类加载器尝试加载。
如果父类加载器无法加载,才由当前类加载器加载。
优点:
1. 避免类的重复加载。
2. 确保核心类库的安全性。
-
-
- 运行时数据区(Runtime Data Areas)
类装载子系统与 JVM 的运行时数据区密切相关:- 方法区(Method Area):存储类的元数据(如类名、方法信息、常量池等)。
- 堆(Heap):存储类的实例对象。
- 栈(Stack):存储方法的局部变量和操作数栈。
- 执行引擎(Execution Engine)
类装载子系统与执行引擎协同工作:- 执行引擎负责执行字节码指令。
- 类装载子系统为执行引擎提供加载后的类信息。
二、运行时数据区
2.1、栈(线程)
JVM栈主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。JVM栈是JVM为每个线程分配的一个独立的内存区域。它是线程私有的,即每个线程都有自己的JVM栈,互不干扰。JVM栈的主要功能是管理Java方法的调用和执行,确保每个方法在执行时都有自己的数据存储空间。具体来说,JVM栈通过栈帧(Stack Frame)的方式管理方法的执行,每个方法调用对应一个栈帧。栈帧是内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
2.1.1栈帧
首先,我们需要了解栈帧是什么。在Java虚拟机(JVM)中,栈帧(Stack Frame)是用于支持方法调用和执行的数据结构,是方法执行时的内存模型。每个方法从调用直至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址等信息。
-
局部变量表
局部变量表是负责存储当前方法的局部变量。它是方法执行期间访问和操作局部变量的内存区域。 -
操作数栈
操作数栈是用于在方法执行过程中存储操作数和中间计算结果内存区域。 -
动态链接
动态链接的作用是将符号引用转换为调用方法的直接引用。这个过程可以分为静态链接和动态链接两种:- 静态链接:如果被调用的目标方法在编译期可知,且运行期间不变时,将符号引用转为直接引用的过程称为静态链接。
- 动态链接:如果被调用的目标方法在编译期无法确定下来,只能在程序运行期间将调用方法的符号引用转为直接引用,这种转换过程具备动态性,所以被称之为动态链接。
动态链接的实现机制与JVM的类加载机制密切相关。在类加载的解析阶段,JVM会将常量池内的符号引用替换为直接引用。这个过程确保了符号引用能够被正确地解析为内存中的直接引用,从而实现Java程序的正常运行。
-
方法返回地址
方法返回地址是存放调用函数执行完毕后返回的地址。
2.2、堆
JVM堆是Java虚拟机(JVM)所管理的内存中最大的一块线程共享的内存区域,用于存储对象实例和数组,是Java程序运行时数据区的核心部分。
2.2.1、JVM堆的组成结构
从内存管理的角度来看,JVM堆可以进一步划分为以下几个区域:
- 新生代(Young Generation):
- Eden区:新对象通常在这里分配内存,大多数对象在这里创建并很快被回收。
- Survivor区:存在两个Survivor区(通常称为From和To),存活下来的对象从Eden区复制到Survivor区。两个Survivor区之间会进行对象交换,存活时间较长的对象最终会晋升到老年代。
- 老年代(Old Generation):
- 存储生命周期较长的对象,如常驻内存中的对象或多次GC后仍未被回收的对象。
- 永久代/元空间(PermGen/Metaspace,JDK 1.8及以后为元空间):
- 在JDK 1.7及以前,永久代用于存储类的元数据、常量池、静态变量等数据。
- 在JDK 1.8及以后,永久代被元空间取代,元空间使用本地内存而不是JVM堆内存。
2.3、JVM方法区
JVM方法区是一个逻辑上的概念,也被称为非堆(Non-Heap)。它主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,线程共享的内存区域。这些数据对于Java虚拟机的正常运行和类加载等都具有重要作用。
2.3.1、存储内容
- 类信息:包括类的结构(如类名、父类名、接口名等)、字段、方法、注解等。这些信息使得虚拟机能够准确地了解每个加载类型的结构和关系。
- 常量池:包含编译期生成的各种字面量和符号引用,如字符串、数字、类和方法的符号引用等。这些常量在运行时供虚拟机使用,提高了程序的性能。
- 静态变量:类的所有实例共享的变量,它们在类加载时被初始化并存储在方法区中。
- 即时编译器编译后的代码:即时编译器(JIT)将高级别的Java代码编译成本地机器码,以提高程序的执行速度。编译后的代码存储在方法区中。
2.4、程序计数器
JVM程序计数器是一块较小的内存区域,用于存储当前线程正在执行的字节码指令的地址,即指向方法区中下一条要执行的字节码指令的指针。它是线程私有的,确保每个线程在执行Java代码时能够准确地定位到下一条要执行的指令。
2.4.1、工作原理
- 线程私有性:每个线程都有自己独立的程序计数器,互不干扰。这保证了在多线程环境下,线程切换后能够正确地恢复到上一个线程执行的位置。
- 指令地址存储:程序计数器存储的是当前线程正在执行的字节码指令的地址,确保线程能够准确地执行下一条指令。当线程调用一个方法时,程序计数器会更新为新方法的第一条指令的地址;当方法正常返回时,程序计数器会恢复到调用方法中的下一条指令。
- 分支与跳转:当执行到字节码中的分支或跳转指令时,程序计数器会更新为跳转目标指令的地址。这确保了程序能够按照预定的顺序执行指令,同时能够灵活地处理分支和子程序调用。
2.4.2、特点与性质
- 内存占用小:程序计数器占用内存很小,在进行JVM内存计算时,可以忽略不计。这也是它运行速度较快的原因之一。
- 无内存溢出风险:由于程序计数器只是存储了一个地址值,并且这个地址值是在方法区中的,不涉及堆或栈的内存分配,因此不会出现内存溢出(OutOfMemoryError)的情况。在JVM规范中,程序计数器是唯一一个没有规定任何OutOfMemoryError情况的区域。
- 线程安全性:由于程序计数器是线程私有的,因此不会发生线程安全问题。这保证了在多线程环境下程序的正确执行与流畅切换。
2.5、本地方法栈
2.5.1、定义与作用
本地方法栈是JVM中为执行本地方法(Native Method)而准备的一块内存区域。本地方法指的是使用Java以外的语言(例如C或C++)编写,并通过Java Native Interface(JNI)或其他方式被Java程序调用的方法。这些方法直接在底层操作系统或硬件上运行,以实现一些Java语言无法直接处理的功能,如操作系统接口调用、硬件驱动程序操作等。
2.5.2、结构特点
- 线程私有:本地方法栈与JVM中的其他内存区域(如堆、方法区)不同,它是线程私有的。这意味着每个线程都有自己独立的本地方法栈空间,互不干扰。
- 动态分配:本地方法栈的内存是动态分配的,根据实际需要自动增长和缩小。然而,如果内存不足,也会抛出StackOverflowError异常。
- 栈帧组成:与Java栈类似,本地方法栈也是由栈帧(Stack Frame)组成的。每次调用本地方法时,JVM会在本地方法栈中为该方法创建一个栈帧,栈帧包含与该本地方法执行相关的信息,如局部变量表、操作数栈、返回地址等。
三、字节码执行引擎
3.1、执行引擎概述
字节码执行引擎是JVM中用于执行Java字节码的部分。Java编译器将Java源代码编译成字节码,这些字节码与平台无关,可以在任何支持JVM的平台上运行。字节码执行引擎的主要功能包括解释字节码、即时编译(JIT)、垃圾回收、异常处理和安全检查等。
3.2、执行引擎的工作原理
执行引擎的工作原理可以分为两个主要部分:解释执行和即时编译(JIT,Just In Time Compiler)。
- 解释执行:
- 当Java程序启动时,执行引擎首先采用解释执行的方式逐条解释字节码指令并执行。
- 解释执行虽然相对较慢,但它具有启动速度快、无需额外编译时间的优点。
- 在解释执行过程中,执行引擎会检查每个字节码指令的合法性,并根据需要加载相应的类、创建对象等,从而确保Java程序在运行时的安全性和稳定性。
- 即时编译(JIT):
- 为了提高Java程序的执行效率,JVM引入了即时编译技术。
- JIT编译器会在程序运行过程中,将热点代码(即频繁执行的代码)编译成机器码并存储起来。
- 当这些热点代码再次被调用时,执行引擎可以直接执行已经编译好的机器码,从而大大提高程序的执行效率。
- 垃圾回收:JVM还负责内存管理,包括自动回收不再使用的对象所占用的内存。垃圾回收机制是JVM的一个重要特性,它减轻了开发者的内存管理负担。
- 异常处理:字节码执行引擎还负责处理程序中的异常情况,确保程序的健壮性。
- 安全检查:为了确保字节码的安全性,防止恶意代码的执行,JVM在加载和执行字节码时会进行一系列的安全检查。
3.3、执行引擎的组件
- 解释器:解释器是执行引擎的一部分,负责逐条解释字节码并执行。在早期JVM中,主要采用的就是解释器执行模式。
- 即时编译器(JIT Compiler):JIT编译器也是执行引擎的重要组成部分,负责将热点代码编译成机器码。
现代JVM中,通常将解释器和JIT编译器结合使用,以权衡编译本地代码的时间和直接解释执行代码的时间。 - 热点探测:热点探测是JIT编译器的一个重要功能,用于识别程序中的热点代码。
HotSpot VM等JVM实现通常会为每个方法建立方法调用计数器和回边计数器,以统计方法的调用次数和循环体的执行次数。当这些计数器的值超过一定阈值时,就会触发JIT编译。
3.4、执行引擎的重要性
- 平台无关性:执行引擎使得Java程序能够在不同的操作系统和硬件平台上运行,实现了“一次编写,到处运行”的目标。
- 安全性:执行引擎在解释和执行字节码时,会进行各种安全检查,确保Java程序在运行时不会违反Java的安全规则。
- 高效性:通过解释执行和即时编译技术,执行引擎能够确保Java程序在运行时具有较高的执行效率。同时,JIT编译器的优化策略还可以进一步提高程序的性能。
- 可移植性:由于执行引擎的存在,Java程序可以轻松地从一个平台移植到另一个平台,无需进行大量的修改和重构工作。
3.5、垃圾回收线程的工作原理
垃圾回收线程的工作原理通常基于以下几种算法:
- 标记-清除算法:这是最基本的垃圾回收算法。它首先标记出所有从根对象(GC Roots)可达的对象,然后清除所有未被标记的对象。这种算法的优点是实现简单,但缺点是会产生内存碎片。
- 标记-整理算法:为了克服内存碎片的问题,标记-整理算法在标记阶段与标记-清除算法相同,但在清除阶段,它会对存活的对象进行整理,消除内存碎片。
- 复制算法:复制算法将内存分为两块,每次只使用其中一块来分配对象。当这一块内存用完时,就将存活的对象复制到另一块内存中,并清空当前块的所有对象。这种算法适用于对象存活率较低的场景。
- 分代收集算法:这是现代JVM广泛采用的垃圾回收算法。它将内存分为年轻代和老年代,根据对象的存活时间和生命周期长短来分别采用不同的回收策略。年轻代通常使用复制算法,而老年代则使用标记-清除或标记-整理算法。
&spm=1001.2101.3001.5002&articleId=145634197&d=1&t=3&u=84035e3471fd4ab6be459564cdf4ee1f)
4754

被折叠的 条评论
为什么被折叠?



