深入理解Java虚拟机(JVM)的运行原理与架构组件

442 阅读12分钟

引言:

今天是端午节前一天,最后再卷一把。

最近项目想要从JDK8直接升级到21,故此再升级的过程中发生了很多不兼容,也顺便学习了一下JVM相关原理和介绍,后续会针对21 最新属性,介绍一下使用虚拟线程(Virtual Threads)这一强大工具。当然目前先从基础(JVM)入手,学习里面构造,原理及后续的如何调优,内存逃逸问题分析等

概述:

JVM(Java虚拟机)是一个可以执行Java字节码的抽象计算机。它是Java平台的核心组成部分,提供了一个平台无关的运行环境,使得Java程序能够在任何具有JVM的设备上运行。JVM负责将Java字节码转换为特定机器上的机器码。

总结JRE、JDK和JVM关系

JRE(Java Runtime Environment)、JDK(Java Development Kit)和JVM(Java Virtual Machine)是Java平台的三个核心组成部分,它们之间存在密切的关系,但各自承担不同的角色:

JVM(Java Virtual Machine)

  • 定义:JVM是一个抽象的计算机,它为Java字节码提供了一个运行时环境。JVM负责执行Java程序的字节码,并提供跨平台的运行能力,即“一次编写,到处运行”(Write Once, Run Anywhere)。
  • 作用:JVM主要负责两件事情:加载编译后的字节码文件到内存中,执行这些字节码。它包括类加载器、运行时数据区、执行引擎和本地库接口等。
  • 独立性:JVM是平台相关的,需要针对不同的操作系统进行实现。不同的操作系统上有不同的JVM实现,但它们都提供了相同的接口。

JRE(Java Runtime Environment)

  • 定义:JRE包含了运行Java程序所需的环境,是JVM的一个超集。它提供了JVM的实现以及运行Java程序必须的库和其他文件。
  • 组成:JRE包括JVM、核心类库(如java.util、java.lang等)和支持文件。如果用户只需要运行Java程序,而不需要开发新的Java程序,那么只安装JRE就足够了。
  • 目的:JRE的目的是为Java程序提供一个运行时环境,但它不包含开发工具(如编译器、调试器等)。

JDK(Java Development Kit)

  • 定义:JDK是提供给Java开发者使用的完整软件开发工具包。它包含了JRE和开发人员编写、编译、调试和监控Java程序所需的工具。
  • 组成:JDK包括JRE、编译器(javac)、Java应用程序打包工具(jar)、文档生成工具(javadoc)和其他工具。
  • 目的:JDK的目的是为Java开发者提供一个完整的开发环境。任何需要开发Java应用程序的人都需要安装JDK。

关系总结

  • JVM:是JRE和JDK的基础,负责Java字节码的执行。
  • JRE:包括JVM和运行Java程序所需的核心类库,是JDK的一部分。
  • JDK:包括JRE和开发Java程序所需的工具,是Java开发的完整环境。

简单来说,JDK用于开发Java程序,JRE用于运行Java程序,而JVM是运行时环境的核心部分,实现了Java程序的跨平台运行。如果只想运行Java程序,只需要JRE;如果想开发Java程序,那么需要安装JDK,因为它包含了JRE。

JVM核心概述

JVM的主要组成部分:

  1. 类加载器(Class Loaders):负责加载类文件到JVM中。
  2. 运行时数据区(Runtime Data Areas):存储运行时数据,包括方法区(Method Area)、堆(Heap)、栈(Stacks)、程序计数器(Program Counter Register)和本地方法栈(Native Method Stacks)。
  3. 执行引擎(Execution Engine):执行字节码,通常包括解释器(Interpreter)和即时编译器(JIT Compiler)。
  4. 本地库接口(Native Interface):连接和处理Java和本地应用程序库之间的调用。
  5. 垃圾收集器(Garbage Collector):自动管理内存,回收不再使用的对象。
classDiagram
    class JVM {
        <<virtual machine>>
        类加载器
        运行时数据区
        执行引擎
        本地库接口
        垃圾收集器
    }
    class ClassLoaders {
        <<class loaders>>
        加载.class文件
    }
    class RuntimeDataAreas {
        <<runtime areas>>
        方法区
        堆
        栈
        程序计数器
        本地方法栈
    }
    class ExecutionEngine {
        <<execution>>
        解释器
        即时编译器
    }
    class NativeInterface {
        <<interface>>
        本地方法调用
    }
    class GarbageCollector {
        <<garbage collection>>
        内存回收
    }

    JVM --|> ClassLoaders : 包含
    JVM --|> RuntimeDataAreas : 包含
    JVM --|> ExecutionEngine : 包含
    JVM --|> NativeInterface : 包含
    JVM --|> GarbageCollector : 包含

JVM的运行原理:

1. 类加载(Class Loading)

JVM通过类加载器(Class Loaders)将Java类加载到内存中。类加载分为三个主要阶段:

  • 加载(Loading):查找并加载类的二进制数据(通常是.class文件)。
  • 链接(Linking)
    • 验证(Verification):确保加载的类符合JVM规范,没有安全问题。
    • 准备(Preparation):为类变量分配内存并设置默认值。
    • 解析(Resolution):将符号引用转换为直接引用。
  • 初始化(Initialization):执行类构造器<clinit>()方法,初始化静态变量和执行静态代码块。

2. 运行时数据区(Runtime Data Areas)

JVM在内存中维护多个运行时数据区域:

  • 方法区(Method Area):存储已被加载的类信息、常量、静态变量、即时编译器编译后的代码等。
  • 堆(Heap):存放对象实例,是垃圾收集器管理的主要区域,分为新生代和老年代。
  • 栈(Java Stack):每个Java线程都有自己的栈,用于存储栈帧。
  • 程序计数器(Program Counter Register):当前线程所执行的字节码的行号指示器。
  • 本地方法栈(Native Method Stack):专门用于处理本地方法执行。

3. 执行引擎(Execution Engine)

执行引擎负责执行类中的方法。它包括:

  • 解释器(Interpreter):快速地逐条解释执行字节码。
  • 即时编译器(JIT Compiler):将热点代码(频繁执行的代码)编译为本地机器码,以提高效率。
  • 垃圾回收器(Garbage Collector):自动回收不再被引用的对象。

4. 本地接口(Java Native Interface,JNI)

JNI定义了一套标准,允许Java代码和本地应用程序(如C、C++编写的应用程序)进行交互。

5. 本地方法库(Native Libraries)

这些是由本地代码(如C、C++)实现的方法库,可以通过JNI被JVM内的Java代码调用。

graph LR
    A[Java源代码] --> B(编译器)
    B --> C(Java字节码)
    C -->|类加载| D[类加载器]
    D -->|加载| E[加载.class文件]
    E -->|链接| F{链接器}
    F -->|验证| G[验证器]
    F -->|准备| H[准备器]
    F -->|解析| I[解析器]
    G --> J(初始化)
    H --> J
    I --> J
    J --> K[方法区]
    K --> L[运行时数据区]
    L -->|对象创建| M[堆]
    M --> N[垃圾收集器]
    N --> M
    L -->|方法调用| O[栈]
    O -->|栈帧操作| P[栈帧]
    P -->|局部变量操作| Q[局部变量表]
    P -->|操作数栈操作| R[操作数栈]
    P -->|动态链接| S[动态链接]
    P -->|方法返回| T[返回地址]
    T --> O
    O -->|执行字节码| U[执行引擎]
	U -->|解释执行| V[解释器]
    U -->|即时编译| W[JIT编译器]
    W -->|编译为本地代码| X[优化的本地代码]
    X --> L
    U -->|调用本地方法| Y[本地库接口]
    Y -->|本地方法执行| Z[本地方法]
    Z --> U

- 编译器将Java源代码转换成Java字节码。
- 类加载器负责加载、链接(验证、准备、解析)和初始化过程。
- 方法区存储类信息,运行时数据区包括堆和栈。
- 堆用于对象的存储,垃圾收集器负责回收不再使用的对象。
- 栈用于存放栈帧,每个栈帧包含局部变量表、操作数栈、动态链接和返回地址。
- 执行引擎包括解释器和JIT编译器,负责执行字节码。
- 本地库接口和本地方法用于执行本地代码。

JVM的设计使Java程序能够在不同的操作系统和平台上运行,而不需要为每个系统重写代码。此外,JVM提供了自动内存管理和垃圾回收,降低了内存泄漏和指针错误的风险,提高了应用程序的稳定性。

JVM加载类

JVM加载类的过程是类加载机制的核心部分,它涉及到类的生命周期中的加载(Loading)、链接(Linking)和初始化(Initialization)阶段。以下是这个过程的详细步骤:### 1. 加载(Loading) 在这个阶段,JVM负责从文件系统或网络源中读取Java类的二进制数据,并将这些数据转换成java.lang.Class类的实例。加载可以由以下类加载器完成:

  • 引导类加载器(Bootstrap Class Loader):它加载Java的核心库(位于<JAVA_HOME>/jre/lib目录或者某些其他认可的位置)。
  • 扩展类加载器(Extension Class Loader):它加载来自<JAVA_HOME>/jre/lib/ext目录(或其他由java.ext.dirs系统属性指定的路径)的类库。
  • 应用程序类加载器(Application Class Loader):它负责加载环境变量classpath或系统属性java.class.path指定路径中的类库。

2. 链接(Linking)

链接阶段负责将类的二进制数据合并到JRE中,它包括以下子阶段:

  • 验证(Verification):确保加载的类符合JVM规范,不会危害JVM自身的安全。验证器检查类的格式、依赖、方法区的内存空间以及字节码等。
  • 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。这些变量的实际值会在初始化阶段赋予。
  • 解析(Resolution):将类、接口、字段和方法的符号引用转换成直接引用。这涉及到查找这些元素在方法区内的实际位置。

3. 初始化(Initialization)

在初始化阶段,JVM负责执行类构造器<clinit>()方法的代码。这个方法由编译器自动收集类中的所有静态初始化器和类变量赋值操作生成。初始化阶段是执行Java代码的第一个阶段,它包括:

  • 执行静态代码块:按照代码中出现的顺序执行静态代码块。
  • 赋值静态变量:赋予程序员设定的初始值,而不是编译器自动分配的默认值。

这个过程是类加载机制的一部分,它确保Java程序能够动态加载类,同时提供了类加载的三个阶段的概念,以确保类在被JVM使用之前是正确的。此外,JVM提供了一种机制,允许开发者自定义类加载器,以扩展或增强应用程序的类加载功能。

graph TD
    start(开始加载类) --> findClass{查找类文件}
    findClass -->|找到.class文件| loadClass[加载.class文件到内存]
    findClass -->|未找到| throwCNE[抛出ClassNotFoundException]
    
    loadClass --> verifyClass[验证类的正确性]
    verifyClass -->|验证失败| throwVE[抛出VerifyError]
    verifyClass -->|验证成功| prepareClass[为静态字段分配内存并设置默认值]
    
    prepareClass --> resolveClass{解析符号引用}
    resolveClass -->|解析失败| throwError[抛出Error]
    resolveClass -->|解析成功| initClass[执行<clinit>方法]
    
    initClass -->|执行静态变量赋值| setStaticVars[赋值静态变量]
    initClass -->|执行静态代码块| execStaticBlocks[执行静态代码块]
    
    setStaticVars --> classLoaded[类加载完成]
    execStaticBlocks --> classLoaded
    classLoaded -->结束加载类

Java双亲委派模型

Java双亲委派模型(Parent Delegation Model)是Java类加载器(ClassLoader)采用的一种特定的工作方式。 在这个模型中,类加载器在尝试加载一个类时,首先会将加载任务委托给其父类加载器去执行。如果父类加载器无法完成这个任务(即它没有找到对应的类),子类加载器才会尝试自己去加载这个类。这个模型的具体工作流程如下:

  1. 当应用程序请求加载一个类时,这个请求首先会被传递给启动类加载器(Bootstrap ClassLoader)。
  2. 启动类加载器会检查自己能否加载这个类,如果能够加载,则直接加载;如果不能,则委托给其子类加载器,也就是扩展类加载器(Extension ClassLoader)。
  3. 扩展类加载器同样会尝试加载这个类,如果能加载则直接加载;如果不能,则委托给其子类加载器,也就是系统类加载器(System ClassLoader)。
  4. 系统类加载器会尝试加载这个类,如果它也不能加载,那么它会尝试从Java应用程序的类路径(classpath)中查找并加载这个类。
  5. 如果所有的类加载器都无法加载这个类,那么会抛出ClassNotFoundException异常。

流程图:

graph TD
    A[开始加载类 'X'] --> B{启动类加载器检查}
    B -->|已加载| B1[返回已加载的类 'X']
    B -->|未加载| C{启动类加载器尝试加载}
    C -->|加载成功| C1[返回启动类加载器加载的类 'X']
    C -->|加载失败| D{扩展类加载器检查}
    D -->|已加载| D1[返回已加载的类 'X']
    D -->|未加载| E{扩展类加载器尝试加载}
    E -->|加载成功| E1[返回扩展类加载器加载的类 'X']
    E -->|加载失败| F{系统类加载器检查}
    F -->|已加载| F1[返回已加载的类 'X']
    F -->|未加载| G{系统类加载器尝试加载}
    G -->|加载成功| G1[返回系统类加载器加载的类 'X']
    G -->|加载失败| H{自定义类加载器检查}
    H -->|已加载| H1[返回已加载的类 'X']
    H -->|未加载| I{自定义类加载器尝试加载}
    I -->|加载成功| I1[返回自定义类加载器加载的类 'X']
    I -->|加载失败| J[抛出ClassNotFoundException]   
	style B1 fill:#f9f,stroke:#333,stroke-width:2px
    style C1 fill:#f9f,stroke:#333,stroke-width:2px
    style D1 fill:#f9f,stroke:#333,stroke-width:2px
    style E1 fill:#f9f,stroke:#333,stroke-width:2px
    style F1 fill:#f9f,stroke:#333,stroke-width:2px
    style G1 fill:#f9f,stroke:#333,stroke-width:2px
    style I1 fill:#f9f,stroke:#333,stroke-width:2px

该描述了双亲委派模型中各个类加载器之间的交互:

  • 开始加载类时,首先会检查启动类加载器是否已经加载了该类。
  • 如果启动类加载器没有加载过该类,它会尝试加载。如果加载成功,则返回该类;如果失败,则继续。
  • 接下来,扩展类加载器会检查是否已加载该类,尝试加载,并根据结果返回类或继续。
  • 系统类加载器重复相同的检查和尝试加载过程。
  • 如果系统类加载器也无法加载类,且存在自定义类加载器,流程会继续到自定义类加载器。
  • 自定义类加载器会检查并尝试加载类。如果成功,则返回该类;如果失败,则抛出ClassNotFoundException

双亲委派模型的优点包括:

  • 避免类的重复加载:由于类加载请求首先由父类加载器处理,因此保证了使用不同的类加载器加载时,最终得到的都是同一个类实例。 -安全性:由于Java核心库的类由启动类加载器加载,这样可以防止恶意代码替换核心库中的类,从而增加了Java平台的安全性。
  • 性能:由于父类加载器通常会缓存已经加载过的类,因此可以提高类加载的效率。

尽管双亲委派模型带来了很多好处,但在某些情况下,比如在OSGi环境中或者热部署功能中,可能需要打破这种模型。为此,Java允许开发者通过创建自定义类加载器并重写loadClass方法的方式来实现特定的加载逻辑,以支持更加灵活的类加载策略。

总结:

JVM作为运行Java字节码的抽象计算机,通过其核心组件—类加载器、运行时数据区、执行引擎和垃圾收集器—实现了从代码编译到程序执行的全过程管理。类加载器采用双亲委派模型保证了类的安全加载,运行时数据区管理了程序的内存使用,执行引擎负责字节码的解释执行或即时编译,而垃圾收集器则高效地回收内存资源。这些组件相互协作,确保了Java应用的稳定运行和系统资源的有效利用。

Java虚拟机(JVM)中的双亲委派模型是一种类加载机制,确保了Java类的安全和一致性。在这个模型中,类加载器在尝试加载一个类之前,会先委托其父类加载器进行加载,从而优先使用高层次的加载器加载类。这种层级化的加载方式避免了类的重复加载,防止了核心类库被篡改,同时也为类隔离和模块化提供了基础。JVM通过这种模型,结合具体的类加载器实现,如启动类加载器、扩展类加载器和应用程序类加载器,协同工作,高效地加载和管理Java程序中的类。