【JVM】感觉弗如…类加载机制
在Java开发过程中,从源代码(.java文件)到字节码(.class文件)再到运行时的类加载,会经历几个关键步骤,我们先简单过一遍大体的过程。再介绍今天这篇博客的重点内容——类加载机制。
.java文件从编译到运行
1. 编写源代码(.java文件)
Java开发者使用文本编辑器或集成开发环境(IDE)编写源代码。源代码是Java语言编写的程序,包含了类定义、方法定义、变量声明等。
2. 编译源代码
编写完源代码后,需要通过Java编译器将源代码编译成字节码。这个过程由javac
命令触发:
javac MyClass.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方法之前?还是在使用类前?接下来我们罗列几个常见的类加载机制出现的时机:
- 首次使用类时:在程序运行期间,当某个类被首次主动使用时,JVM会开始这个类的加载阶段。主动使用的情况包括但不限于创建类的实例、访问某个类的静态变量、调用类的静态方法等。
- 通过类加载器显式加载:当通过Java类加载器(如
java.lang.ClassLoader
的子类)显式加载一个类时,也会触发该类的加载阶段。 - 由其他类引用:当一个类在运行时使用了另一个类,JVM可能需要加载并初始化那个被使用的类。
- 初始化某个类的子类:如果一个类的子类被加载,其父类还未被加载,JVM会先加载父类。
- 调用类的静态方法:在调用一个类中的静态方法时,该类会被加载。
示例如上,我们编写了一个用于测试的DemoTest类,一个TestHaHa类,在main中调用TestHaHa的静态方法,即使用了TestHaHa类,此时触发类加载机制,加载了TestHaHa中的静态代码块。
其余示例由于篇幅原因,不在本篇博客中作展示。
类从加载到使用
下图展示了Java类生命周期的主要阶段,包括编译、加载、连接、初始化、使用和卸载。,由于类加载只包括加载、链接、初始化三个过程,故而本篇博客暂时不会提及最终的卸载环节。
1. 加载
加载是一个读取Class文件,将其转化为某种静态数据结构存储在方法区内,并在堆中生成一个便于用户调用的java.lang.Class类型的对象的过程。
-
通过类的全限定名查找类:JVM通过类加载器查找.class文件或提供.class文件的网络资源。
-
将.class文件的二进制数据读入JVM:这些数据被存储在方法区内。
-
在堆区创建java.lang.Class对象:每个类在JVM中都有唯一对应的Class对象,用于表示类在JVM的状态。
2. 验证
- 首先对文件格式进行验证(发生于加载阶段)
- 而后对元数据和字节码进行验证(即对Class静态结构进行语法和语义上的分析,保证其不会产生危害虚拟机的行为)
- 对符号引用进行验证(在解析阶段进行)
3. 准备
准备阶段是在字节码验证通过之后,虚拟机会认为该Class是安全的,此时将会进入准备阶段。
准备阶段做的处理其实不复杂,就是为类分配静态变量的内存,并设置默认初始值。例如,对于基本数据类型,int会被初始化为0,对象引用会被初始化为null。
在准备阶段的介绍中,我们简单了解方法区。
在JDK 8之前,方法区通常与永久代(Permanent Generation,PermGen)联系在一起。在JDK 8及以后弃用了永久代这种实现方式,采用**元空间(Metaspace)**这种直接内存来取代。
有人说,JDK 8以后采用了元空间来替代方法区,这种说法是完全错误的。
因为方法区是抽象概念,元空间是实现方式。
在JDK 8之前,类的元信息、常量池、静态变量等都存储在永久代这种具体实现中,而在JDK 8及以后,常量池、静态变量等被移除方法区,从而转移到了堆中,元信息这些依然保留在方法区内,但是具体的存储方式改成了元空间。
4. 解析
解析阶段是在准备阶段完成之后,主要做的一件事情就是将符号引用替换为直接引用。
回到刚刚说的将符号引用替换为直接引用。
那么什么是符号引用,什么是直接引用呢?
我们首先假设类A与类B
当类A在运行时需要引用类B时,JVM会通过以下步骤来解析这个引用:
- 类加载:如果类B尚未被加载,JVM将触发类B的加载过程。
- 解析:JVM解析类A中的符号引用,找到类B的
Class
对象。 - 替换:JVM将符号引用替换为直接引用,这个直接引用指向类B的确切位置。
我在这篇博客中提到过动态链接的概念:
【JVM】从i++到JVM栈帧-CSDN博客
为什么这里也有将符号引用转换为直接引用的操作呢?这里的操作与类加载中的操作有什么区别?
当一个动态调用发生时,JVM会执行以下操作:
当解析部分完成,外部链接的Java类已经成功地引入到了程序当中。
5. 初始化
在Java的类加载机制中,初始化阶段标志着类加载过程的完成。在这一阶段,JVM会识别并执行类级别上的初始化操作,这些操作与new一个对象不同。
初始化动作包括以下几个方面:
- 类变量的赋值:在类级别上声明的变量(即静态变量)将根据代码中的指令被赋予具体的初始值。
- 静态代码块的执行:类中定义的静态代码块(以
static { ... }
括起来的部分)包含了一些在类加载时需要执行的逻辑,这些逻辑会在初始化阶段按代码中出现的顺序执行。
值得注意的是,这些类级别的初始化与调用对象的构造函数是两个不同的概念。对象的实例化涉及到以下步骤:
- 使用
new
关键字:当Java代码中显式使用new
关键字创建一个对象时,会触发对象的构造过程。 - 构造函数的调用:对象实例化后,JVM会调用与对象相对应的构造函数(用
<init>()
表示),以完成对象初始化。
类初始化和对象初始化是两个不同的阶段,分别对应于类和对象的不同生命周期。类初始化是一次性的,发生在类首次加载到JVM时;而对象初始化则在每次创建新对象时发生,为每个对象单独进行。
结语
接下来还会继续介绍JVM的类加载器和双亲委派机制,笔者争取争取都给学一下。
已经没有啦!不用再往下翻啦!