【JVM】感觉弗如...类加载机制

【JVM】感觉弗如…类加载机制

image-20240512123540411

在Java开发过程中,从源代码(.java文件)到字节码(.class文件)再到运行时的类加载,会经历几个关键步骤,我们先简单过一遍大体的过程。再介绍今天这篇博客的重点内容——类加载机制

本篇博客配图大部分源自:

【JVM】Java类加载机制这块算是玩明白了_哔哩哔哩_bilibili

.java文件从编译到运行

image-20240510201707196

1. 编写源代码(.java文件)

Java开发者使用文本编辑器或集成开发环境(IDE)编写源代码。源代码是Java语言编写的程序,包含了类定义、方法定义、变量声明等。

简单来说就是我们写在编译器里面的东西.jpg

2. 编译源代码

编写完源代码后,需要通过Java编译器将源代码编译成字节码。这个过程由javac命令触发:

javac MyClass.java

编译过程涉及以下步骤:

  • 词法分析:编译器读取源代码文件,将其分解成一系列的词法单元(tokens),如关键字、标识符、符号等。
  • 语法分析:编译器检查词法单元的排列顺序是否符合Java语言的语法规则。
  • 语义分析:编译器检查代码的语义,如类型检查、变量作用域等。
  • 生成字节码:编译器将经过分析的源代码转换成Java虚拟机的字节码指令。

简单来说就是将人类可读的Java源代码转换成JVM可执行的字节码

3. 生成字节码(.class文件)

编译过程结束后,会生成一个或多个.class文件,每个.class文件对应源代码中的一个公共类(public class)。字节码是JVM能够理解的中间代码,包含了Java源代码的执行逻辑,但不包含任何特定于硬件平台的指令。(这也是Java跨平台的基础机制之一)

4. 运行Java程序

生成.class文件后,可以使用java命令来运行程序:

java MyClass

运行过程涉及以下步骤:

  • 加载:JVM的类加载器负责加载.class文件到JVM中,创建java.lang.Class对象。
  • 链接:链接过程包括验证字节码的合法性、为静态变量分配内存并设置默认值、将符号引用转换为直接引用。
  • 初始化:执行类构造器<clinit>()方法,初始化静态变量和静态代码块。
  • 执行:JVM开始执行程序的main方法,程序按照编写的逻辑运行。

5. 类加载阶段发生的时机

了解过java的编译运行流程,仍有一点不清楚,类加载阶段究竟发生在什么时候?是在进入main方法之前?还是在使用类前?接下来我们罗列几个常见的类加载机制出现的时机:

  1. 首次使用类时:在程序运行期间,当某个类被首次主动使用时,JVM会开始这个类的加载阶段。主动使用的情况包括但不限于创建类的实例、访问某个类的静态变量、调用类的静态方法等。
  2. 通过类加载器显式加载:当通过Java类加载器(如java.lang.ClassLoader的子类)显式加载一个类时,也会触发该类的加载阶段。
  3. 由其他类引用:当一个类在运行时使用了另一个类,JVM可能需要加载并初始化那个被使用的类。
  4. 初始化某个类的子类:如果一个类的子类被加载,其父类还未被加载,JVM会先加载父类。
  5. 调用类的静态方法:在调用一个类中的静态方法时,该类会被加载。

image-20240512125943184

示例如上,我们编写了一个用于测试的DemoTest类,一个TestHaHa类,在main中调用TestHaHa静态方法,即使用了TestHaHa类,此时触发类加载机制,加载了TestHaHa中的静态代码块。

其余示例由于篇幅原因,不在本篇博客中作展示。

类从加载到使用

下图展示了Java类生命周期的主要阶段,包括编译、加载、连接、初始化、使用和卸载。,由于类加载只包括加载、链接、初始化三个过程,故而本篇博客暂时不会提及最终的卸载环节。

想了想还是把简单的概述放在这里吧~

卸载:当类不再被使用时,由JVM的垃圾回收器卸载。

image-20240510201742436

1. 加载

加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。

  1. 通过类的全限定名查找类:JVM通过类加载器查找.class文件或提供.class文件的网络资源。

  2. 将.class文件的二进制数据读入JVM:这些数据被存储在方法区内。

  3. 在堆区创建java.lang.Class对象:每个类在JVM中都有唯一对应的Class对象,用于表示类在JVM的状态。

注1:此处的Class文件并不一定指的是本地文件,而是泛指各种来源的二进制流(网络、数据库、及时生成的Class文件)

注2:全限定名:

全限定名通常由以下两部分组成:

  1. 包名(Package Name):类或接口所属的包的名称,用句点(.)分隔。
  2. 类名或成员名(Class or Member Name):类的简单名称或接口的简单名称,以及可能的方法名和字段名。

假设有一个名为MyClass的类,它位于名为com.example的包中,那么它的全限定名将是:

com.example.MyClass

如果MyClass类中有一个名为myMethod的方法,这个方法的全限定名将是:

com.example.MyClass.myMethod

2. 验证

  1. 首先对文件格式进行验证(发生于加载阶段)
  2. 而后对元数据和字节码进行验证(即对Class静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为)
  3. 符号引用进行验证(在解析阶段进行)

image-20240510203535267

3. 准备

准备阶段是在字节码验证通过之后,虚拟机会认为该Class是安全的,此时将会进入准备阶段。

准备阶段做的处理其实不复杂,就是为类分配静态变量的内存,并设置默认初始值。例如,对于基本数据类型,int会被初始化为0,对象引用会被初始化为null。

这里仅仅是静态变量,而不是成员变量。

在准备阶段的介绍中,我们简单了解方法区。

在JDK 8之前,方法区通常与永久代(Permanent Generation,PermGen)联系在一起。在JDK 8及以后弃用了永久代这种实现方式,采用**元空间(Metaspace)**这种直接内存来取代。

永久代与元空间的区别:

永久代是堆内存中的一部分,用于存储类元数据。但是,由于永久代的大小是固定的,这可能导致内存溢出问题。

元空间位于本地内存(Native Memory),而不是堆内存。这意味着元空间的大小只受限于本地内存的大小,而不是JVM堆内存的大小。

有人说,JDK 8以后采用了元空间来替代方法区,这种说法是完全错误的。

因为方法区是抽象概念,元空间是实现方式。

在JDK 8之前,类的元信息、常量池、静态变量等都存储在永久代这种具体实现中,而在JDK 8及以后,常量池、静态变量等被移除方法区,从而转移到了中,元信息这些依然保留在方法区内,但是具体的存储方式改成了元空间。

元信息包括:

  1. 类信息:包括类的名称、访问修饰符、继承关系、接口实现等。
  2. 字段信息:包括字段的名称、类型、访问修饰符等。
  3. 方法信息:包括方法的名称、返回类型、参数列表、异常表、访问修饰符等。
  4. 字节码指令:JVM执行的指令序列,用于实现类中定义的方法的具体行为。
  5. 类加载器信息:标识用于加载类的类加载器。

image-20240510203922435

4. 解析

解析阶段是在准备阶段完成之后,主要做的一件事情就是将符号引用替换为直接引用

注:事实上,解析阶段还有许多其他任务,例如:

类和接口的解析、自读但解析、类方法解析、实例方法解析,延迟解析(将某些引用推迟到运行时再进行解析)

回到刚刚说的将符号引用替换为直接引用。

那么什么是符号引用,什么是直接引用呢?

我们首先假设类A与类B

符号引用(Symbolic Reference)

符号引用是编译时使用的一种引用形式,它通过一系列描述性的符号来引用目标。在Java类编译成.class文件后,如果类A中引用了类B,此时A中的引用会以符号引用的形式存在。这个符号引用通常是一个字符串,它代表了B的全限定名,如"com.example.B"

直接引用(Direct Reference)

直接引用是类加载过程中解析阶段的一个输出,它是一个具体的指针,指向目标对象或目标类的内存地址。一旦类B被加载和链接到JVM中,JVM就会将类A中的符号引用替换为直接引用,这个直接引用指向类B的java.lang.Class对象或方法的具体内存地址。

当类A在运行时需要引用类B时,JVM会通过以下步骤来解析这个引用:

  1. 类加载:如果类B尚未被加载,JVM将触发类B的加载过程。
  2. 解析:JVM解析类A中的符号引用,找到类B的Class对象。
  3. 替换:JVM将符号引用替换为直接引用,这个直接引用指向类B的确切位置。

我在这篇博客中提到过动态链接的概念:

【JVM】从i++到JVM栈帧-CSDN博客

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为**符号引用(Symbolic Reference)**保存在class文件的常量池里。

比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

为什么这里也有将符号引用转换为直接引用的操作呢?这里的操作与类加载中的操作有什么区别

静态解析:如果调用的目标在编译时就已经明确,如调用一个具体类的方法,那么解析发生在编译后的链接阶段。

动态解析:如果调用的目标在编译时不明确,如调用一个抽象类或接口的方法,且该方法在运行时可能有不同的实现,那么解析将推迟到运行时,这称为动态绑定

当一个动态调用发生时,JVM会执行以下操作:

  1. 运行时类型检查:JVM检查对象的实际类型。
  2. 方法选择:JVM根据对象的实际类型选择正确的方法版本。
  3. 直接引用替换:JVM使用选定方法的实际内存地址来替换原来的符号引用。

当解析部分完成,外部链接的Java类已经成功地引入到了程序当中。

image-20240510204038490

5. 初始化

在Java的类加载机制中,初始化阶段标志着类加载过程的完成。在这一阶段,JVM会识别并执行类级别上的初始化操作,这些操作与new一个对象不同。

初始化动作包括以下几个方面:

  1. 类变量的赋值:在类级别上声明的变量(即静态变量)将根据代码中的指令被赋予具体的初始值。
  2. 静态代码块的执行:类中定义的静态代码块(以static { ... }括起来的部分)包含了一些在类加载时需要执行的逻辑,这些逻辑会在初始化阶段按代码中出现的顺序执行。

值得注意的是,这些类级别的初始化调用对象的构造函数是两个不同的概念。对象的实例化涉及到以下步骤:

  1. 使用new关键字:当Java代码中显式使用new关键字创建一个对象时,会触发对象的构造过程。
  2. 构造函数的调用:对象实例化后,JVM会调用与对象相对应的构造函数(用<init>()表示),以完成对象初始化。

类初始化和对象初始化是两个不同的阶段,分别对应于类和对象的不同生命周期。类初始化是一次性的,发生在类首次加载到JVM时;而对象初始化则在每次创建新对象时发生为每个对象单独进行

结语

本篇博客内容参考:

【JVM】Java类加载机制这块算是玩明白了_哔哩哔哩_bilibili

接下来还会继续介绍JVM的类加载器和双亲委派机制,笔者争取争取都给学一下。

已经没有啦!不用再往下翻啦!

  • 9
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值