目录
一、类加载过程
一个类型从被加载到JVM内存中开始,到卸载出内存为止,它的生命周期有7个阶段:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),如下图所示。其中加载、验证、准备、初始化、卸载的开始具有顺序性,但可以交叉进行。以下详细介绍其各个阶段。
1. 加载(Loading)
加载(Loading)目的是将类的Class文件加载到内存,由类加载器完成加载。
2. 验证(Verification)
验证(Verification)目的确保Class文件包含的信息符合《Java 虚拟机规范》的全部约束要求,验证阶段是否严谨,直接决定JVM是否能承受恶意代码的攻击。注意:文件格式验证后才能存储到元空间,其他三个验证基于元空间存储结构上进行,不再读取字节流。
3. 准备(Preparation)
准备(Preparation) 目的将类中静态变量(static)分配内存并设置初始值(默认值)。
4. 解析(Resolution)
解析(Resolution)目的将常量池的符号引用替换为直接引用。引用有:
- 符号引用:一组符号来描述引用的目标(与内存布局无关);
- 直接引用:直接指向目标的指针、偏移量、目标的句柄(与内存布局有关)。
解析点主要是类或接口、字段、类方法、接口方法等,其中:JDK9之前除了接口方法(public修饰)无需进行访问权限校验,其他查找结束后都要进行访问权限校验,不通过则抛出:java.lang.IllegalAccessError,JDK9之后增加了模块化访问权限;除invokedynamic指令外,会对第一次解析结果进行缓存,称为“解析缓存”。
解析点 | 解析步骤 |
类或接口 | 当前代码处在D类,根据未解析的符号引用N去解析类或接口C的直接引用,步骤如下: step1:C非数组类型,全限定名N传递给D的类加载器,去加载C,根据C的加载动作,也可能触发其他类的相关加载动作,一旦有任何异常,则解析失败; step2:C数组类型,若元素是引用对象,则根据step1进行加载解析; 若元素是基本类型,则JVM自动生成代表该数组维度和元素类型; step3:确认D是否对C有访问权限,否则抛出java.lang.IllegalAccessError; step4:没有任何异常,则已形成有效的类或接口。 |
字段 | 当前代码处在D类,根据未解析的符号引用N去解析字段(字段所属的类或接口C),步骤如下: step1:首先解析D字段表class_index的索引CONSTANT_Class_info类进行解析(字段所在的类解析),后续对C进行字段搜索; step2:C本身包含了简单名称和描述符与目标字段匹配,返回该字段的直接引用,则查找结束; step3:否则,则查找C实现的接口和其父接口,从下往上递归搜索,有匹配,则查找结束; step4:否则,若C不是Object类,则查找C的父类,从下往上递归搜索,有匹配,则查找结束; step5:否则,查找失败,则抛出java.lang.NoSuchFieldError; step6:查找成功后,D是否对该字段有访问权限,否则抛出:java.lang.IllegalAccessError。 |
类方法 | 当前代码处在D类,根据未解析的符号引用N去解析方法(方法所属的类C),步骤如下: step1:首先解析D方法表class_index的索引CONSTANT_Class_info类进行解析(字段所在的类解析),若C是一个接口(与接口方法相反),则抛出:java.lang.IncompatibleClassChangeError; step2:在类C本身包含了简单名称和描述符与目标方法匹配,返回该方法的直接引用,则查找结束; step3:否则,若C不是Object类,则查找C的父类,从下往上递归搜索,有匹配,则查找结束; step4:否则,则查找C实现的接口和其父接口,从下往上递归搜索,有匹配说明C是个抽象类,则抛出:java.lang.AbstractMethodError; step5:否则,查找失败,则抛出java.lang.NoSuchMethodError; step6:查找成功后,是否对该方法有访问权限,否则抛出:java.lang.IllegalAccessError。 |
接口方法 | 当前代码处在D类,根据未解析的符号引用N去解析方法(方法所属的接口C),步骤如下: step1:首先解析D方法表class_index的索引CONSTANT_Class_info类进行解析(字段所在的类解析),若C是一个类(与类方法相反),则抛出:java.lang.IncompatibleClassChangeError; step2:在接口C本身包含了简单名称和描述符与目标方法匹配,返回该方法的直接引用,则查找结束; step3:否则,则查找C的父接口,从下往上递归搜索直至Object类(接口方法包含Object类中方法),有匹配,则查找结束; step4:否则,则查找C实现的接口和其父接口,从下往上递归搜索,若不同的父接口中有多个匹配的方法时,返回其中一个方法,则查找结束; step5:否则,查找失败,则抛出java.lang.NoSuchMethodError。 |
注意: a.JDK9之前除了接口方法(public修饰)无需进行访问权限校验,其他查找结束后都要进行访问权限校验,不通过则抛出:java.lang.IllegalAccessError;JDK9之后增加了模块化访问权限; b.类方法和接口方法存储的地方不同: 类方法CONSTANT_Methodref_info、接口方法CONSTANT_InterfaceMethodref_info。 |
5. 初始化(Initialization)
初始化(Initialization)目的是执行类构造器<clinit>()方法的过程。类初始化<clinit>()在编译期自动生成,且自动收集类变量并赋值和静态语句块的语句合并产生。
注意:类构造器<clinit>()方法,先隐式调用父类该方法;而实例构造器<init>()方法,则显式调用父类构造器。
二、类加载时机
1. 主动引用(引用时主动初始化)
主动引用是引用时主动初始化,有且只有6种需立即初始化,如下图所示。
2. 被动引用(引用时不触发初始化)
被动引用是引用时不触发初始化,如下三种情况:
1):第一种被动引用
package com.cmmon.instance.classLoad;
/**
* @author tcm
* @version 1.0.0
* @description 被动引用:引用时不触发初始化 —— 子类引用父类的静态字段,不会导致子类初始化
* @date 2023/5/18 10:22
**/
public class NotInitialization {
/**
* 静态字段时,只有直接定义这个字段的类才会被初始化
* 因此:通过子类来引用父类定义的静态字段,则只会初始化父类而不初始化子类
*
* 添加参数:-XX:+TraceClassLoading会导致子类加载
* [Loaded com.cmmon.instance.classLoad.SuperClass from file:/E:/idea%20project/instance-test/target/test-classes/]
* [Loaded com.cmmon.instance.classLoad.SubClass from file:/E:/idea%20project/instance-test/target/test-classes/]
*/
public static void main(String[] args) {
System.out.println(SubClass.value);
/*
结果:并没有初始化子类SubClass
SuperClass init!
1234
*/
}
}
class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 1234;
}
class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
2):第二种被动引用
package com.cmmon.instance.classLoad;
/**
* @author tcm
* @version 1.0.0
* @description 被动引用:引用时不触发初始化 —— 数组来引用类,不会导致该类的初始化
* @date 2023/5/18 11:40
**/
public class NotInitialization2 {
/**
* 通过数组来引用类,不会导致该类的初始化
*/
public static void main(String[] args) {
SuperClass2[] sca = new SuperClass2[10];
/*
没有输出:SuperClass init!
*/
}
}
class SuperClass2 {
static {
System.out.println("SuperClass init!");
}
public static int value = 1234;
}
3):第三种被动引用
package com.cmmon.instance.classLoad;
/**
* @author tcm
* @version 1.0.0
* @description 被动引用:引用时不触发初始化 —— 常量在编译阶段会加入到调用类的常量池中,不会导致常量所在的类初始化
* @date 2023/5/18 17:33
**/
public class NotInitialization3 {
/**
* 编译阶段通过常量传播优化后,常量直接存入到调用类的常量池中,不会导致常量所在的类初始化
* 因此:NotInitialization3与ConstClass两个类在编译成Class文件后,则不存在任何联系了
*/
public static void main(String[] args) {
System.out.println(ConstClass.HELLO_WORLD);
/*
没有输出:ConstClass init!
*/
}
}
class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLO_WORLD = "Hello World";
}
三、类加载器
1. 加载器类型
类加载过程中加载(Loading)阶段是由类加载器完成,如下表所示根据不同层面对类加载器进行分类。注意:类的唯一性由类全限定名且类加载器共同决定,每个类加载器都一个独立的类名称空间。
分类层面 | 分类 | 特点 |
JVM角度分类 | 启动类加载器 (Bootstrap ClassLoader) | 1.JVM自身的一部分; 2.由C++实现。 |
其他类加载器 | 1.全部继承java.lang.ClassLoader; 2.由Java语言实现。 | |
三层类加载器 (JDK8与之前版本) | 启动类加载器 (Bootstrap ClassLoader) | 1.负责加载${JAVA_HOME}/lib目录的类库; 2.该加载器被Java程序直接引用,JVM自身的一部分。 |
扩展类加载器 (Extension ClassLoader) | 1.负责加载${JAVA_HOME}/lib/ext目录的类库; 2.由sun.misc.Launcher.ExtClassLoader类实现; | |
应用程序类加载器 (Application ClassLoader) | 1.负责加载用户类路径classpath路径下的所有类库; 2.由sun.misc.Launcher.AppClassLoader类实现; | |
自定义类加载器 (User ClassLoader) | 用户自定义类加载器 | |
模块化类加载器 (JDK9) | 与JDK8有些变动 | 1.平台类加载器(PlatForm ClassLoader)代替扩展类加载器; 2.取消${JAVA_HOME}/lib/ext、${JAVA_HOME}/jre目录; 3.平台类加载器和应用程序类加载器不在继承java.net.URLClassLoader,而继承BuiltinClassLoader。 |
注意: a.类的唯一性:类的全限定名 + 类加载器,每个类加载器都一个独立的类名称空间; b.类加载器之间的层次关系,称为:“双亲委派模型”(Parents Delegation Model); c.父类加载器是否为null,来引导类加载器达到双亲委派,为null则使用启动类加载器。 |
如下代码所示,比较两个类是否相等:是否同一个全限定名的类 + 类加载器相同。
package com.cmmon.instance.classLoad;
import java.io.IOException;
import java.io.InputStream;
/**
* @author tcm
* @version 1.0.0
* @description 每个类加载器拥有一个独立的类名称空间
* @date 2023/5/24 14:17
**/
public class ClassLoadNamespace {
/**
* 比较两个类是否相等:是否同一个全限定名的类 + 类加载器相同
* 每个类加载器拥有一个独立的类名称空间
* 两个类的“相等”:类的Class对象的哈希码、equals()、isAssignableFrom()、instanceof
*/
public static void main(String[] args) throws Exception {
// 自定义类加载器
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
};
Object object = myLoader.loadClass("com.cmmon.instance.classLoad.ClassLoadNamespace").newInstance();
System.out.println(object.getClass()); // class com.cmmon.instance.classLoad.ClassLoadNamespace
System.out.println(object instanceof com.cmmon.instance.classLoad.ClassLoadNamespace); // false
System.out.println(object.getClass().getClassLoader()); // com.cmmon.instance.classLoad.ClassLoadNamespace$1@7b3300e5
Object object2 = new ClassLoadNamespace();
System.out.println(object2.getClass()); // class com.cmmon.instance.classLoad.ClassLoadNamespace
System.out.println(object2 instanceof com.cmmon.instance.classLoad.ClassLoadNamespace); // true
System.out.println(object2.getClass().getClassLoader()); // sun.misc.Launcher$AppClassLoader@18b4aac2
}
}
2. 双亲委派模型
如下图所示,双亲委派执行过程及其优点。JDK9维持三层类加载器和双亲委派,变动了委派给父类时,先判定该类是否归属到哪一个模块中,如有则委派给负责该模块的加载器。
父类加载器是否为null,来引导类加载器达到双亲委派,为null则使用启动类加载器。核心方法为java.lang.ClassLoader#loadClass(java.lang.String, boolean),如下源代码所示。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
3. 破坏双亲委派
双亲委派模型并不是强制约束的模型,而是Java设计器推荐给开发者的类加载实现方式。所以Java中大部分类加载器都遵循双亲委派模型,但也有例外,如:Tomcat、OSGi(动态模块化规范 _ 灵活的类加载器架构)。
Tomcat6之前的类库目录如下图所示。
Tomcat服务器加载器架构及对应的委派关系如下。
四、参考资料
HotSpot虚拟机之Class文件及字节码指令_爱我所爱0505的博客-CSDN博客
jvm之java类加载机制和类加载器(ClassLoader)的详解_超级战斗王的博客-CSDN博客