1. 引言
Java虚拟机(JVM)在执行Java程序时,涉及三个重要过程:加载、链接和初始化。这些过程确保Java程序能够正确运行并符合语言规范。加载是将类或接口的二进制数据读入内存并转换为JVM内部的数据结构的过程;链接将这些数据整合到JVM的运行时状态中,包括验证、准备和解析;初始化是执行类构造器<clinit>
方法的过程,用于初始化静态变量和执行静态代码块。通过深入了解这些过程,开发小伙伴们可以更好地理解和优化Java应用。
另外开发小伙伴们可以在读该文章的时候理解成我们在 java -jar 也好 java 直接启动也好,是我们再启动一个程序
2. 加载
加载是JVM执行Java程序的第一步,它将类或接口的二进制数据读入内存,并将其转化为JVM内部的数据结构。
2.1 类加载的过程
类加载过程主要包括以下几个步骤:
- 通过类加载器读取类文件:类加载器(ClassLoader)负责从文件系统、网络或其他来源读取类文件。
- 将二进制数据转化为Class对象:将类文件的二进制数据转化为JVM可理解的Class对象。
- 分配内存:为新加载的类或接口分配内存,并初始化为默认值。
2.2 类加载器的类型
JVM中有三种主要的类加载器:
- 引导类加载器(Bootstrap ClassLoader):负责加载核心类库,如
rt.jar
。 - 扩展类加载器(Extension ClassLoader):负责加载扩展类库,如
ext
目录中的类。 - 应用类加载器(Application ClassLoader):负责加载应用程序的类和库。
当然这三种类加载器我们在介绍jar命令(java 溯本求源之基础(七)之 jar(上篇)-CSDN博客)的时候已经已经说过了,感兴趣的可以简单看一下
2.3 类加载器的工作机制
双亲委派模型是类加载器的核心机制。具体工作流程如下:
- 类加载请求:当类加载器收到一个类加载请求时,它首先检查该类是否已经被加载。
- 委派给父类加载器:如果没有被加载,类加载器将请求委派给父类加载器。
- 父类加载器处理请求:父类加载器继续向上委派,直到引导类加载器。
- 加载类:如果父类加载器能够完成加载,则返回加载的类。如果不能,则由当前类加载器尝试加载类。
双亲委派模型保证了核心类库的统一性,避免了类的重复加载和冲突。
3. 链接
链接是将类或接口的二进制数据整合到JVM运行时状态的过程,包括验证、准备和解析。
3.1 验证
验证确保类文件的正确性和安全性,防止恶意代码执行。主要检查以下内容:
3.1.1 文件格式验证
验证类文件的格式是否符合JVM规范。
3.1.2 字节码验证
检查字节码指令的正确性和安全性,确保没有非法指令。
3.1.3 符号引用验证
验证类、字段、方法的引用是否有效,确保符号引用能够正确解析。
3.2 准备
准备阶段为类的静态变量分配内存,并初始化为默认值。这一步并不执行代码,只是设置内存布局。
3.3 解析
解析是将常量池中的符号引用转化为直接引用。主要包括以下几个步骤:
3.3.1 类或接口解析
将符号引用转化为类或接口的直接引用。
3.3.2 字段解析
将符号引用转化为字段的直接引用。
3.3.3 方法解析
将符号引用转化为方法的直接引用。
4. 初始化
初始化是执行类构造器<clinit>
方法的过程,用于初始化静态变量和执行静态代码块。
类构造器<clinit>
方法
类构造器<clinit>
是JVM在类初始化时执行的特殊方法。它由编译器生成,用于初始化类的静态变量和静态代码块。每个类或接口最多有一个<clinit>
方法,且无参数、无返回值。<clinit>
方法在类加载和链接后,由JVM调用。
4.1 类初始化的条件
根据JVM规范,类或接口在以下几种情况下会被初始化:
-
执行特定的JVM指令:
- new:创建类实例的指令。例如,
MyClass obj = new MyClass();
。当JVM执行new
指令时,如果类未初始化,则会先初始化类。 - getstatic:读取静态字段的指令。例如,
System.out
。当JVM执行getstatic
指令时,如果字段所在的类未初始化,则会先初始化类。 - putstatic:写入静态字段的指令。例如,
MyClass.staticField = value;
。当JVM执行putstatic
指令时,如果字段所在的类未初始化,则会先初始化类。 - invokestatic:调用静态方法的指令。例如,
Math.abs(value)
。当JVM执行invokestatic
指令时,如果方法所在的类未初始化,则会先初始化类。
- new:创建类实例的指令。例如,
-
方法句柄解析:首次调用通过方法句柄解析得到的
java.lang.invoke.MethodHandle
实例。这些方法句柄包括:- REF_getStatic:获取静态字段的方法句柄。
- REF_putStatic:设置静态字段的方法句柄。
- REF_invokeStatic:调用静态方法的方法句柄。
- REF_newInvokeSpecial:调用构造方法的方法句柄。
-
反射调用:通过反射API对类进行反射调用。例如,使用
Class.forName("MyClass")
。 -
子类初始化:当一个类的子类被初始化时,父类也会被初始化。
-
接口方法实现:实现接口的类被初始化时,如果接口中有非抽象方法。
-
JVM启动:JVM启动时指定的主类,例如通过
java -jar
命令启动的主类。
4.2 初始化过程
初始化过程涉及多个步骤,以确保线程安全并处理可能的递归初始化请求:
- 获取初始化锁:每个类或接口都有一个唯一的初始化锁,用于同步初始化过程。
- 检查初始化状态:如果类正在被其他线程初始化,当前线程将等待直到初始化完成。
- 初始化静态字段:按顺序初始化类或接口的所有
final static
字段。 - 初始化超类和超接口:如果类的超类或超接口尚未初始化,递归地对其进行初始化。
- 执行初始化方法:执行类或接口的初始化方法(
<clinit>
)。 - 处理异常:如果初始化过程中抛出异常,设置类为错误状态并通知所有等待线程。
5. JVM启动过程概述
JVM启动过程包括加载主类、执行main
方法,并初始化所有依赖类。这个过程确保应用程序的正常启动和运行。
5.1 加载主类
当执行java -jar
命令时,JVM根据JAR文件中的清单(manifest)加载指定的主类。主类是包含main
方法的类,作为程序的入口。
5.2 执行main
方法
主类的main
方法被调用,这是Java应用程序的起点。main
方法的签名是public static void main(String[] args)
,它接受一个字符串数组作为参数,用于传递命令行参数。
5.3 初始化依赖类
在主类执行过程中,所有被引用的类和接口会按照需要进行加载、链接和初始化。这个过程包括:
- 加载类:通过类加载器读取类文件,并将其转化为JVM内部数据结构。
- 链接类:包括验证、准备和解析,将类文件整合到JVM的运行时状态中。
- 初始化类:执行类构造器
<clinit>
方法,初始化静态变量和执行静态代码块。
JVM启动过程确保了Java应用程序的所有必要类和资源都已正确加载和初始化,从而保证程序的正常运行。
6. 类的卸载
类的卸载是JVM在运行过程中将不再使用的类从内存中移除的过程。类的卸载有助于优化内存使用,避免内存泄漏。主要步骤包括:
- 确定不再使用的类:通过垃圾回收机制,确定哪些类不再被引用。
- 释放内存:将这些类占用的内存释放。
6.1 类卸载的条件
类的卸载需要满足以下条件:
- 类的所有实例都已被垃圾回收。
- 该类的ClassLoader已被垃圾回收。
- 没有对该类的静态变量和方法的引用。
6.2 类卸载的过程
类卸载过程由垃圾回收器(GC)负责,主要包括以下步骤:
- 标记阶段:GC标记所有不再被引用的类。
- 清除阶段:GC释放被标记的类及其相关资源。
类卸载的过程在优化内存使用方面起到了重要作用,特别是在长时间运行的应用程序中。
再强调下,后面会单独说垃圾回收!!!
7. 总结
加载、链接和初始化是Java虚拟机在执行Java程序时的关键步骤。理解这些过程有助于开发小伙伴们更好地理解,调试和优化Java应用,提升代码的稳定性和性能。JVM的启动过程确保了应用程序的正常运行,而类的卸载则有助于优化内存使用,避免内存泄漏。
最后希望各位开发小伙伴点赞收藏加评论!!!!谢谢拉!!!!