类的生命周期
一个.class文件从被加载到JVM内存,到被卸载,一共经历了7个阶段:加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载,其中验证、准备、解析统称为链接。
加载
- 通过类的全限定名来获取定义类的二进制字节流(来源包括
.class文件、Zip/Jar包、网络传输、动态代理运行时生成等)。 - 将字节流中的静态存储结构转化为方法区的运行时数据结构。
- 在堆内存中生成一个代表该类的
java.lang.Class对象,作为程序访问该类方法区中数据的外部入口。
验证
保证字节流对应的Class文件的信息符合《Java虚拟机规范》,不会危害虚拟机安全。
其中包含:
- 文件格式验证:验证二进制字节流符合Class文件格式规范,能够被当前版本JVM处理。
- 魔数检查:文件的前4个字节必须是
0xCAFEBABE。 - 版本号检查:主次版本号是否在当前JVM能够处理的范围内,不能会抛
UnsupportedClassVersionError。 - 常量池检查:常量池中的常量类型是否存在不支持的类型标签,索引是否指向了不存在的常量或不合法的常量。
- UTF-8编码检查:
CONSTANT_Utf8_info的常量中是否存在不符合UTF-8编码的数据。
- 魔数检查:文件的前4个字节必须是
- 元数据验证:验证类的元数据(语法结构和语义)是否符合Java语言规范。
- 继承合法性:该类是否有父类(除
java.lang.Object外都有父类)。 - Final约束:该类是否继承了被
final修饰的类。 - 抽象类约束:如果该类不是抽象类,是否实现了父类或接口中要求实现的所有方法。
- 重写/重载合法性:类中的字段、方法是否与父类产生矛盾。
- 继承合法性:该类是否有父类(除
- 字节码验证:通过数据流和控制流分析,确定程序语义是合法的。
- 类型安全:保证操作数栈的数据类型与字节码指令正常配合工作。
- 跳转安全:保证方法中的控制流跳转指令(如
goto)不会跳转到方法体之外的字节码指令上。 - 类型转换安全:保证方法体中的类型转换是有效的。
- 符号引用验证:发生在解析阶段将符号引用转化为直接引用时,验证类是否缺少或无权访问其引用的外部类、字段或方法。
- 类存在性:符号引用中通过全限定名能否找到对应类。
- 成员存在性:指定类中是否存在符号引用描述和的字段或方法。
- 访问权限:符号引用中的类、字段、方法的可访问性是否允许当前类进行访问。
准备
为类的静态变量分配内存并设置初始值(零值)。
对于final修饰的基本数据类型/String常量,在编译时javac会为该变量生成ConstantValue放入常量池,准备阶段直接赋值为常量值。
其他静态变量设置初始值,如0、false、null等。
解析
将Class文件常量池中的符号引用替换为直接引用。
- 符号引用:用一组符号来描述目标,不依赖内存布局。
- 直接引用:直接指向目标的内存指针、相对偏移量或间接能够定位到目标的句柄,与JVM内存布局相关。
解析的对象包括类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等。
解析分为静态解析和动态解析(延迟解析): - 静态解析:在链接阶段就将常量池所有符号引用一次性解析完毕。
- 动态解析/延迟解析:真正使用该符号引用时才去解析该符号(如
getstatic等)。
初始化
执行类构造器<clinit>()方法的过程。
<clinit>():由编译器自动收集类中所有类变量的赋值动作和静态代码块(static{}) ,按源代码顺序合并生成<clinit>()方法。
特点:
- 执行顺序:父类的
<clinit>()先于子类<clinit>()执行。接口的<clinit>()执行时不需要先执行父接口的<clinit>()。 - 线程安全:JVM会保证一个类的
<clinit>()方法在多线程环境中被正确地加锁同步。
初始化的时机
- 字节码指令触发:遇到
new,getstatic,putstatic、invokestatic指令时。 - 反射调用:使用
java.lang.reflect包的方法对类进行反射调用时,如Class.forName()。 - 父类初始化:初始化子类时,如果父类还没初始化,先触发父类的初始化。
- 启动主类:被指定的执行主类(包含
main())会被初始化。 - 动态语言支持(JDK 7+):使用
java.lang.invoke.MethodHandle实例解析出的方法句柄对应的类未初始化时。 - 接口默认方法:初始化一个接口的实现类时,如果接口定义了默认方法,会先触发接口的初始化。
不会初始化的场景
- 通过子类引用父类的静态字段:只会触发父类的初始化,不会触发子类的初始化。
System.out.println(SubClass.superStaticField); // 只初始化 SuperClass- 通过数组引用来定义类:数组类是独立的类,对应的类不会初始化。
User[] users = new User[10]; // User 类不会被初始化- 编译期常量:编译期会将常量存入调用类的常量池,调用时不会引用定义常量的类。
public class MyClass {
public static final String HELLO = "hello world";
// ...
}
System.out.println(MyClass.HELLO); // MyClass 不会被初始化类加载器
类加载器负责将.class文件加载到JVM内存中,生成对应的Class对象。
层次结构
flowchart BT Bootstrap["Bootstrap ClassLoader"] Ext["Extension ClassLoader"] App["Application ClassLoader"] Custom["Custom ClassLoader"] Custom --> App App --> Ext Ext --> Bootstrap
Bootstrap ClassLoader(启动类加载器)
由C++实现,属于虚拟机的一部分,在Java代码中获取其引用会返回null。
加载<JAVA_HOME>/lib目录下或-Xbootclasspath指定的路径下的jar包(如rt.jar、tools.jar等)。
无父加载器。
Extension ClassLoader(扩展类加载器)
由Java实现,位于sun.misc.Launcher$ExtClassLoader。
加载<JAVA_HOME>/lib/ext目录下或系统变量java.ext.dirs指定的路径下的jar包。
父加载器为Bootstrap ClassLoader。
JDK9模块化改动
ExtClassLoader被重命名为Platform ClassLoader,取消了 lib/ext 目录,负责加载模块化平台 API。
Application ClassLoader(应用程序类加载器)
由Java实现,位于sun.misc.Launcher$AppClassLoader,是程序默认的类加载器(ClassLoader.getSystemClassLoader())。
加载用户类路径classpath下的类。
父加载器为Extension ClassLoader。
Custom ClassLoader(自定义类加载器)
用户编写的继承自ClassLoader的类加载器。
父加载器为Application ClassLoader。
相关方法:
protected Class loadClass(String name, boolean resolve) // 重写可打破双亲委派
protected Class findClass(String name) // 重写遵循双亲委派双亲委派模型
JDK在1.2引入了双亲委派模型。
原理
一个类加载器的加载过程:
- 自底向上检查:当收到类加载请求时,先检查自己的缓存中是否加载过:
- 若有则直接返回对应的Class对象
- 若自己没有加载过,则将请求委派给父加载器,直到Bootstrap ClassLoader。
- 自顶向下加载:当父加载器无法完成加载时,自己才会尝试加载类。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已经被加载过
Class<?> c = findLoadedClass(name);
if (c == null) { // 没加载过
long t0 = System.nanoTime();
try {
if (parent != null) { // 委派给父加载器
c = parent.loadClass(name, false);
} else { // 父加载器为null说明是Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 父类加载器抛出ClassNotFoundException表示父加载器加载范围内找不到该类
}
if (c == null) {
// 父加载器加载失败后,调用本加载器的findClass尝试加载
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c); // 解析类
}
return c;
}
}原因
- 安全性:防止核心类被篡改。如果用户写了
java.lang.String等类,最终委派给Bootstrap ClassLoader加载,Bootstrap ClassLoader会保证加载的是JDK自带的类。 - 避免重复加载:只有类全限定名相同和由同一个加载器加载的类才是相同类。防止不同加载器加载同一个类,导致内存中出现多个
Class对象。
JVM核心类的安全
- 双亲委派模型保证了核心类被Bootstrap ClassLoader加载,防止被篡改。
- 验证阶段ClassLoader.defineClass()会校验包名等是否与核心类相同,非法的会抛出SecurityException。
打破双亲委派模型
重写loadClass()方法
将loadClass()中先将加载请求委派给父加载器的部分去掉。
线程上下文类加载器(TCCL)
Thread.currentThread().getContextClassLoader()可以获取当前线程上下文的类加载器(默认为Application ClassLoader),父加载器可以将加载请求反向委派给子加载器。
JDBC驱动等核心类接口由Bootstrap ClassLoader加载,但驱动实现类在项目的 ClassPath 下的第三方jar包中,Bootstrap ClassLoader无法加载这些类,便会委派给子加载器加载。
ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);中ServiceLoader.load用的就是TCCL。
自定义加载器和模块化框架
Tomcat为每个容器创建一个WebAppClassLoader,对于用户编写的类优先自己加载。
- 应用隔离:不同Web应用可能使用不同版本的相同类,需要进行隔离,因此每个应用使用独立的WebAppClassLoader,优先加载本应用 /WEB-INF/classes 和 /WEB-INF/lib 下的类。
- 热部署:重新部署应用时,只需要丢弃该应用的WebAppClassLoader,重新创建WebAppClassLoader并加载即可实现热部署。
- 容器安全:Tomcat的容器实现类由CatalinaClassLoader加载,对Web应用隔离,防止Tomcat被修改。
- 资源共享:通过Common ClassLoader和Shared ClassLoader,统一加载多个Web应用共享的类,节省内存。
flowchart BT Bootstrap["Bootstrap ClassLoader"] Ext["Extension ClassLoader"] App["Application ClassLoader"] Common["Common ClassLoader (Tomcat 基础公共类)"] Catalina["Catalina ClassLoader (Tomcat 容器内部私有类)"] Shared["Shared ClassLoader (WebApp 共享类)"] WebApp["WebApp ClassLoader (应用私有类,优先自身查找,每个Web应用一个)"] WebApp --> Shared Shared --> Common Catalina --> Common Common --> App App --> Ext Ext --> Bootstrap