聊一聊Java的JVM类加载机制
作者:索码理
Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。当Java程序运行时,Java虚拟机会按需加载类,即在程序需要使用某个类时才会加载该类。
类的生命周期如下图:
JVM的类加载机制包括加载、连接( 验证、准备、解析)、初始化 3个阶段。
加载(Loading)
加载(Loading) 阶段主要是查找并加载字节码文件,这个文件可以是来自本地文件系统、网络、jar包等地方。加载后,生成一个对应的Class对象。
加载类时会做以下工作:
- 根据类的全限定名查找并读取类的二进制数据。类的二进制数据可以来自文件、网络、数据库等各种数据源。
- 将类的二进制数据转换成方法区内部的数据结构。在转换的过程中,JVM会对类的二进制数据进行解析和校验。
- 在方法区内存储该类的相关信息,包括类的名称、修饰符、常量池、字段描述符、方法描述符、接口描述符、方法表等。
- 生成一个代表该类的Class对象,并将该对象存放在JVM的堆内存中。Class对象包含了类的各种信息,可以用于创建类的实例、获取类的方法和字段等操作。
需要注意的是,在加载类的过程中,JVM会遵循一定的双亲委派机制,即先委派给父类加载器尝试加载,如果父类加载器无法加载,则由当前类加载器进行加载。这样可以保证类的加载不会重复,避免出现类似的类被多次加载的情况。有关类加载器可以查看我之前的文章。
加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。
连接(Linking)
连接阶段是Java虚拟机将类文件中的符号引用转换为直接引用的过程,会对字节码文件进行验证、准备、解析。
- 验证(Verification):在这个阶段,JVM会对字节码进行验证,以确保其符合Java虚拟机规范,并且不会对虚拟机造成安全威胁。验证的内容包括静态分析、字节码验证、符号引用验证等。如果验证失败,JVM会抛出VerifyError异常。
- 准备(Preparation):在这个阶段,JVM会为类的静态变量分配内存,并将其初始化为默认值(零值)。这个阶段不会执行任何Java代码,只是为静态变量分配内存空间。例如将int类型的静态变量赋值为0。
public static int staticValue = 123;
准备阶段初始化只是将静态变量初始化为默认值,比如上面这段代码,不同数据类型都有其默认值,初始化是只是将staticValue
赋予默认值0
,也就是staticValue = 0
,只有在初始化阶段才会将staticValue
赋值为123,也就是staticValue = 123
。但是如果是staticValue是个常量public static final int staticValue = 123
,准备阶段才会将staticValue
赋值为123。
- 解析(Resolution):在这个阶段,JVM会将类、接口、字段和方法的符号引用解析为直接引用。符号引用是指用来描述某个类、字段或方法的名称和类型的符号,而直接引用则是指直接指向内存中的具体位置的引用。解析的过程包括将类或接口的符号引用解析为直接引用、将字段的符号引用解析为直接引用、将类中方法的符号引用解析为直接引用。
初始化(Initialization)
初始化阶段是指在类被首次主动使用时执行的阶段,虚拟机会执行类的初始化代码,包括静态变量的赋值和静态代码块的执行等操作。
初始化阶段是类加载的最后一个阶段,在该阶段,JVM会执行以下操作:
- 执行静态变量赋值操作:在这个阶段,JVM会对所有的静态变量进行初始化并赋值。这些静态变量的值通常在类定义中已经被明确定义了,JVM会根据定义进行相应的赋值操作。
- 执行静态代码块:如果类定义中包含有静态代码块,那么在该阶段JVM会执行这些静态代码块中的代码。
- 调用类的初始化方法:在Java程序中,可以使用static关键字来定义一个类的静态初始化方法(即static void methodName())。在该阶段,JVM会调用这个类的静态初始化方法。
类初始化的时机
类初始化时机包括以下四种情况:
- 创建该类的实例对象时,例如使用 new 关键字创建对象,类的初始化将被触发 如果一个类是程序执行的入口类(即包含 main() 方法的类),那么也会触发该类的初始化操作。
- 子类初始化会触发父类初始化:当一个子类被初始化时,其父类也会被初始化。这意味着,如果一个类没有被使用,那么它的父类也不会被初始化。
- 当调用类的静态方法(不包括final方法和private方法)或访问类的静态字段(不包括final字段)时,类的初始化将被触发。
- 当使用反射API对类进行某些操作时(例如使用Class.forName()方法加载类、调用Class.newInstance()方法创建对象、调用Method.invoke()方法调用方法等),类的初始化将被触发。
初始化是线程安全的JVM保证一个类的初始化只会由一个线程去执行,其他线程需要等待该线程完成后才能访问该类。
下面用一个简单的Java代码示例,展示JVM类加载机制中初始化阶段的示例
public class MyClass { // 静态变量 public static String staticStr = "Hello, world!"; static { System.out.println("MyClass is initialized."); } // 构造方法 public MyClass() { System.out.println("MyClass constructor is called."); } // 静态方法 public static void staticMethod() { System.out.println("MyClass staticMethod is called."); } }
在上述代码中,类MyClass
包含一个静态变量staticStr
、一个静态代码块和一个构造方法,以及一个静态方法staticMethod
。当程序首次使用MyClass
类时,JVM将会触发MyClass类的初始化阶段。可以通过下面的代码来测试类的初始化:
public class Test { public static void main(String[] args) { System.out.println(MyClass.staticStr); // 调用静态变量,触发类初始化 MyClass.staticMethod(); // 调用静态方法,触发类初始化 MyClass obj = new MyClass(); // 创建对象,触发类初始化 } }
在上面的代码中,首先输出了MyClass类的静态变量staticStr
,此时会触发MyClass类的初始化;然后调用了静态方法staticMethod
,同样会触发MyClass类的初始化;最后创建了一个MyClass对象,也会触发MyClass类的初始化。运行上述代码,可以看到以下输出:
MyClass is initialized. Hello, world! MyClass staticMethod is called. MyClass constructor is called.
输出结果表明,MyClass类的初始化确实在首次使用该类时被触发,包括静态变量、静态代码块、静态方法和构造方法都被执行了。
此外,如果一个类是另一个类的子类,那么在使用子类时,父类也会被初始化。例如:
public class MyBaseClass { static { System.out.println("MyBaseClass is initialized."); } } public class MySubClass extends MyBaseClass { static { System.out.println("MySubClass is initialized."); } } public class Test { public static void main(String[] args) { MySubClass obj = new MySubClass(); // 创建子类对象,触发父类和子类初始化 } }
在上述代码中,当创建MySubClass类的对象时,将会触发MyBaseClass和MySubClass类的初始化。运行上述代码,可以看到以下输出:
MyBaseClass is initialized. MySubClass is initialized.
总结
JVM的类加载机制采用了延迟加载的策略,即在需要使用类时才加载该类,这种方式可以提高程序的启动速度,也避免了不必要的资源浪费。同时,JVM还提供了多个类加载器,可以通过自定义类加载器实现特定的加载策略,例如动态加载、远程加载等,从而满足不同的应用需求。
到此这篇关于聊一聊Java的JVM类加载机制的文章就介绍到这了,更多相关JVM类加载机制内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!