Java类加载的过程可以细分为四个主要阶段:加载(Loading)、验证(Verification)、准备(Preparation)和初始化(Initialization)。每个阶段都有其特定的任务和目的。
1. 加载(Loading)
加载是类加载的第一步,它负责将类的二进制数据读入JVM内存,并转换为运行时数据结构。加载过程包括:
- 通过类的全限定名获取定义此类的二进制字节流:这个字节流可以从文件系统、网络、数据库或者其他地方获取。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构:这一步涉及创建类的运行时表示,比如方法表、字段表等。
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口:这个Class
对象是反射的基础,也是JVM内部用来管理类的重要对象。
在加载阶段,JVM会检查是否已经加载过这个类,如果已经加载过,就不再重新加载,直接返回已有的 Class
对象。
2. 链接(Linking)
链接阶段分为三个小步骤:验证、准备和解析。
-
验证(Verification)
- 文件格式验证:确保类文件遵循Java类文件格式。
- 元数据验证:检查类的元数据信息是否正确,比如类是否有合法的父类,是否实现了所有抽象方法等。
- 字节码验证:确保字节码不会导致任何安全问题,比如非法数据转换、非法跳转等。
- 符号引用验证:确保类中引用的其他类、字段和方法确实存在。
-
准备(Preparation)
- 为类的静态变量分配内存,并设置默认初始值。对于基本类型,会设置为零值(如
int
为 0,boolean
为false
);对于引用类型,会设置为null
。
- 为类的静态变量分配内存,并设置默认初始值。对于基本类型,会设置为零值(如
-
解析(Resolution)
- 将类、接口、字段和方法的符号引用转换为直接引用。符号引用是在编译时生成的,而直接引用是在运行时解析得到的。
3. 初始化(Initialization)
初始化阶段是类加载的最后一步,它包括执行类的初始化代码。这包括:
- 执行类的静态变量赋值语句。
- 执行静态代码块。
初始化阶段是按需发生的,只有在真正使用到类的时候才会进行初始化。初始化阶段确保类的静态成员被正确地赋值和初始化。
触发类加载的情况
类加载可以分为显式和隐式两种情况:
显式加载
-
使用
Class.forName
方法:Class<?> myClass = Class.forName("com.example.MyClass");
这里,
"com.example.MyClass"
是类名,它被传递给默认的应用程序类加载器,后者会尝试加载这个类。 -
类加载器对象可以调用
loadClass
方法来加载类:ClassLoader loader = new MyClassLoader(); Class<?> myClass = loader.loadClass("com.example.MyClass");
类名同样是直接作为参数提供的。
-
在自定义类加载器中,通常会重写
findClass
方法来实现具体的加载逻辑:public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = loadClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] loadClassData(String className) { // 从特定位置加载类的字节码 // 例如从文件系统、网络或其他来源 return new byte[0]; // 返回加载的字节数组 } }
隐式加载
-
创建类的实例:
MyClass obj = new MyClass();
这里不需要直接提供类名,因为JVM能从
new
表达式中推断出类名。 -
访问静态成员或调用静态方法:
int value = MyClass.STATIC_FIELD;
在这种情况下,
MyClass
会被加载。 -
初始化子类:
SubClass sub = new SubClass();
即使
SubClass
直接被创建,JVM也会加载SubClass
及其所有的超类。 -
使用反射API:
Method method = MyClass.class.getMethod("someMethod", String.class);
如果
MyClass
或String
类尚未被加载,JVM会先加载它们。 -
接口实现:
List<String> list = new ArrayList<>();
这里
List
和ArrayList
都会被加载。 -
服务提供者加载: Java服务提供者框架允许通过配置文件来指定服务提供者类。例如,在
META-INF/services/com.example.Service
文件中指定了提供者类名,当程序请求服务时,JVM会加载这些类。 -
动态代理:
InvocationHandler handler = ...; MyInterface proxyInstance = (MyInterface) Proxy.newProxyInstance( MyInterface.class.getClassLoader(), new Class[] { MyInterface.class }, handler );
这里
MyInterface
会被加载。
双亲委派模型
Java类加载器采用双亲委派模型,这意味着当一个类加载器接收到类加载请求时,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。每一层的类加载器都是如此,因此所有的请求最终都应该传送到顶层的启动类加载器。只有当父类加载器反馈自己无法完成这个加载请求(即在它的搜索范围中没有找到所需的类)时,子类加载器才会尝试自己去加载。
这种模型的好处在于确保了类的唯一性,避免了不同类加载器加载同一个类的多个版本,从而提高了安全性。
自定义类加载器
如果需要特殊的类加载行为,比如从网络加载类、加密/解密类文件等,可以创建自定义类加载器。自定义类加载器通常继承自 java.lang.ClassLoader
,并重写 findClass
方法来实现具体的加载逻辑。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
// 从特定位置加载类的字节码
// 例如从文件系统、网络或其他来源
return new byte[0]; // 返回加载的字节数组
}
}
通过上述步骤,你可以详细了解类加载的整个过程,包括类加载器如何获得类名、类加载的具体步骤以及双亲委派模型的工作方式。
标签:Java,字节,MyClass,classData,new,Class,加载 From: https://blog.csdn.net/m0_50742275/article/details/143662373