Bootstrap

JVM基本结构(详细)

目录

类加载子系统

方法区

Java 堆

直接内存

Java 栈

本地方法栈

垃圾回收系统

PC 寄存器

执行引擎


类加载子系统

  • 功能概述:类加载子系统在 Java 虚拟机(JVM)中起着至关重要的作用,它负责从各种来源(如文件系统、网络等)加载 Class 信息,并将其妥善存放在方法区中。除此之外,还会细致处理类的链接和初始化等一系列相关操作,以确保类在 JVM 中能够正确运行。
  • 类加载过程详解
    • 装载(Loading):这是类加载的起始步骤,主要任务是查找并加载类的二进制数据。它会根据类的全限定名,在相应的位置(如文件系统中的.class 文件所在路径等)去寻找对应的类文件,并将其读取到内存中,为后续的处理做好准备。
    • 链接(Linking):链接过程又进一步细分为三个子阶段。
      • 验证(Verification):此阶段的关键在于确保被加载类的正确性。它会对类文件的格式、字节码的语法结构等多方面进行严格检查,防止出现格式错误、恶意篡改等情况,保证类的合法性和安全性,例如检查类文件是否符合 Java 虚拟机规范所规定的格式要求。
      • 准备(Preparation):在这个阶段,正式为类变量分配内存空间,并设置其初始值。需要注意的是,这里设置的初始值通常是数据类型对应的默认值,而不是在类中定义的初始值。比如对于int类型的类变量,此时会被设置为 0。
      • 解析(Resolution):解析阶段主要是将常量池中的符号引用替换为直接引用。符号引用是一种在编译阶段使用的、以特定格式表示的对其他类、方法、字段等的引用方式,而直接引用则是能够直接定位到目标的实际内存地址或偏移量等的引用形式。通过解析,使得类在运行时能够准确找到所需要引用的其他类或成员等。
    • 初始化(Initialization):这是类加载过程的最后一步,主要是执行类构造器,对静态变量和静态代码块进行初始化操作。在这个阶段,会按照类中定义的顺序,依次执行静态变量的赋值语句和静态代码块中的代码,从而完成类的初始化,使得类处于可以正常使用的状态。
  • 类加载器分类及职责
    • 启动类加载器(Bootstrap ClassLoader):它是 JVM 内置的类加载器,负责加载位于 /lib 下面的核心类库,这些核心类库包含了 Java 语言运行的基础类,如java.lang包中的各类基础类等。启动类加载器是用 C++ 实现的,并且它是 JVM 的一部分,在 Java 程序启动时就开始发挥作用,为整个程序的运行提供最基本的类支持。
    • 扩展类加载器(Extension ClassLoader):主要负责加载 <java_runtime_home>/lib/ext 或者由系统变量 - djava.ext.dir 指定位置中的类库。它在类加载层次结构中位于启动类加载器之下,承担着加载一些扩展类库的任务,这些扩展类库可以为 Java 程序提供一些额外的功能或特性。
    • 系统类加载器(Application ClassLoader):也被称为应用程序类加载器,它负责加载系统类路径 java-classpath 或 - djava.class.path 变量所指目录下的类库。这是我们在日常开发中最常接触到的类加载器,因为它负责加载我们自己编写的应用程序中的各类类文件,使得我们的应用程序能够正常运行。
  • 双亲委派机制:这是 Java 类加载机制中的一个重要特性。当类加载器收到加载类的请求时,它并不会立刻自己去尝试加载,而是会先将请求委托给父类加载器。只有在父类加载器无法完成加载任务时,才会由自己来尝试加载。这种机制保证了 Java 类的加载具有层次结构,有效避免了类的重复加载问题,同时也极大地降低了核心类库被篡改的风险。例如,当应用程序类加载器收到加载java.lang.String类的请求时,它会先将请求委托给扩展类加载器,扩展类加载器又会将请求委托给启动类加载器,由于启动类加载器能够加载该类,所以就不会再由下面的类加载器进行重复加载了。

方法区

  • 存储内容详解:方法区是 JVM 中用于存储多种重要信息的区域。它主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。具体来说,对于每个被加载的类,方法区会为其分配相应的空间,并且在这个空间中持有该类的 this 和 super 的引用,以便在类的运行过程中能够方便地获取和使用这些信息。
  • 内存管理特点:方法区的内存大小并非是固定不变的,Java 虚拟机可以根据应用程序的实际需要动态地对其进行调整。而且,方法区的内存空间不一定是连续的,它可以在一个堆中自由地进行分配,以适应不同的存储需求和运行情况。

Java 堆

  • 存储对象特性:Java 堆几乎是所有 Java 对象实例的存放之地,是 Java 程序最主要的内存工作区域,并且它是被所有线程所共享的。这意味着在多线程的 Java 程序中,不同线程都可以访问和操作存放在 Java 堆中的对象。
  • 内存划分及目的:由于现在大多数垃圾收集器采用分代收集算法,所以 Java 堆通常被划分为新生代和老年代。新生代又进一步细分为 Eden 区和两个 Survivor 区,这种划分方式主要是为了更高效地进行垃圾回收操作。在新生代中,大多数对象的生命周期相对较短,通过这种划分,可以利用特定的垃圾回收算法(如复制算法)针对不同区域的特点进行更有效的垃圾回收处理,从而提高整个垃圾回收系统的效率。

直接内存

  • 特点说明:直接内存是位于 Java 堆外的、直接向系统申请的内存区间。Java 的 NIO 库允许 Java 程序使用直接内存,它具有一定的特点。通常情况下,访问直接内存的速度会优于 Java 堆内存的访问速度,这是因为它绕过了一些 Java 堆内存访问所需要的中间环节。然而,它的大小也受限于操作系统能给出的最大内存,所以在使用直接内存时,也需要根据实际情况合理控制其使用量,避免出现内存不足等问题。

Java 栈

  • 线程私有属性:每一个线程都拥有私有的 Java 栈,这个 Java 栈是在线程被创建时一同被创建出来的,并且它的生命周期与线程的生命周期是相同的。这意味着每个线程都有自己独立的 Java 栈空间,用于存储与该线程相关的特定信息。
  • 存储信息详情:Java 栈主要保存着局部变量、方法参数等信息,并且对象的引用也可以存在于栈中。更为重要的是,每个方法在执行时都会创建一个栈帧,栈帧中存储了局部变量表、操作数栈、动态连接、方法出口等信息。局部变量表用于存储方法中的局部变量,操作数栈则用于在方法执行过程中进行各种运算操作,动态连接负责实现方法调用时的相关连接操作,方法出口则记录了方法执行完毕后应该返回的位置等信息,通过这些信息的协同作用,使得方法能够在 Java 栈中顺利执行。

本地方法栈

  • 作用阐述:本地方法栈的主要作用是用于支持本地方法的执行。它与 Java 虚拟机栈类似,但它主要是针对那些被 native 修饰的方法(即本地方法)的调用而设置的。这些本地方法通常是使用 C 或 C++ 等其他语言编写的,当 Java 程序需要调用这些本地方法时,就会通过本地方法栈来进行相应的交互操作,以实现对本地方法的正确调用和执行。

垃圾回收系统

  • 回收范围界定:垃圾回收系统在 JVM 中负责对多个区域进行回收操作,主要包括方法区、Java 堆和直接内存等区域,其中 Java 堆是垃圾回收的重点关注对象,因为 Java 堆中存放着大量的 Java 对象实例,这些对象在程序运行过程中会不断地产生和消亡,需要及时进行回收处理,以释放内存空间。
  • 回收方式特点:在 Java 中,所有的对象空间释放是隐式的,也就是说,垃圾回收系统会在后台自动地查找、标识并释放那些被认定为垃圾的对象,从而完成对内存的全自动化管理。这种隐式的回收方式使得开发人员无需在代码中显式地去释放对象内存,大大简化了开发流程,但同时也需要开发人员对垃圾回收机制有一定的了解,以便在出现内存相关问题时能够正确分析和解决。

PC 寄存器

  • 线程私有特性:每个线程都拥有自己独立的 PC 寄存器,这是一种线程私有的内存结构。它的存在保证了每个线程在执行过程中都有自己独立的记录机制,用于存储当前线程所执行的字节码的行号指示器。
  • 作用体现:在多线程环境下,当一个线程被暂停时,通过 PC 寄存器来记录该线程执行到的位置,以便在下次切换回来时能够继续执行。例如,在一个多线程的 Java 程序中,当某个线程因为时间片用完等原因被暂停执行时,它所执行到的字节码行号就会被记录在该线程的 PC 寄存器中,等到下一次该线程再次获得执行机会时,就可以根据 PC 寄存器中记录的行号继续执行下去,保证了线程执行的连续性。

执行引擎

  • 核心功能阐述:执行引擎是 JVM 最核心的组件之一,它肩负着执行字节码的重要任务。通过对字节码的执行,将 Java 程序中的逻辑转化为实际的机器操作,从而使得 Java 程序能够在计算机上正常运行。
  • 执行方式特点:在执行字节码时,执行引擎采用即时编译技术将字节码编译成机器码后再执行,以提高执行效率。在 HotSpot JVM 中,执行引擎包含解释器和 JIT 编译器。解释器的作用是将字节码文件中的内容翻译为对应平台的本地机器码指令,这种方式在程序启动初期速度较快,但随着程序的运行,执行效率可能会相对较低。而 JIT 编译器则会对热点代码(即那些频繁执行的代码)进行深度优化,将其从字节码编译成机器码,并缓存到方法区,这样在后续执行这些热点代码时,就可以直接使用已经编译好的机器码,大大提高了执行效率。
;