类加载机制与类加载器是Java虚拟机(JVM)中非常重要的概念,它们共同管理着Java类的加载过程,确保类的正确性和安全性。
以下是对类加载机制与类加载器的详细解析:
一、类加载机制
类加载机制是指虚拟机将Class文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的Java类型的机制。
一个类从被加载到虚拟机内存中开始,到卸载出内存为止,
它的整个生命周期包括以下几个阶段:
-
1. 加载(Loading):
-
• 通过一个类的全限定名来获取定义此类的二进制字节流。
-
• 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
-
• 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
-
-
2. 验证(Verification):
-
• 确保Class文件的字节流中的信息是符合虚拟机要求的。包括文件格式验证、元数据验证、字节码验证和符号引用验证。
-
-
3. 准备(Preparation):
-
• 为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置初始值(零值)。这些变量所使用的内存都将在方法区进行分配。
-
-
4. 解析(Resolution):
-
• 将常量池内的符号引用替换为直接引用的过程。
-
-
5. 初始化(Initialization):
-
• 执行构造器
()方法(由编译器产生)的过程,为类变量赋正确的初始值。
-
-
6. 使用(Using):
-
• 类被加载并初始化完成后,就可以被虚拟机使用,执行其方法、访问其变量等。
-
-
7. 卸载(Unloading):
-
• 如果类不再被需要,且满足卸载条件(如类加载器被回收、类对象没有被引用等),则该类将被卸载出内存。
-
二、类加载器
类加载器是Java虚拟机的一部分,负责将Java类的二进制代码加载到内存中,并转换为可执行的Java字节码。它是Java语言的重要特性之一,为Java应用程序提供了动态加载和运行时扩展的能力。类加载器的主要职责是根据类的全限定名在运行时定位并读取类的字节码。
Java虚拟机中内置了多种类加载器,它们之间存在父子关系,这种关系称为双亲委派模型。双亲委派模型的工作过程大致如下:
• 当一个类加载器需要加载一个类时,它会先委派给父类加载器加载。
• 如果父类加载器无法加载该类(即父加载器搜索范围中没有找到该类),则子类加载器才会尝试自己去加载。
这种机制的好处包括:
• 保证安全性:通过双亲委派机制,可以防止恶意类的加载和执行。
• 避免重复加载:确保同一个类只被加载一次,节省内存空间。
• 保证类的唯一性:在Java虚拟机中,一个类由其全限定类名和其类加载器共同确定其唯一性。
Java虚拟机中内置的主要类加载器包括:
• 启动类加载器(Bootstrap ClassLoader):由C++实现,负责加载Java核心类库,如rt.jar等。它是Java类加载层次中最顶层的类加载器。
• 扩展类加载器(Extension ClassLoader):由Java实现,负责加载Java扩展目录(如JAVA_HOME/lib/ext)下的类。
• 应用程序类加载器(Application ClassLoader,也称为系统类加载器System ClassLoader):由Java实现,负责加载用户类路径(ClassPath)上的类,包括用户自定义的类。
除了这些内置的类加载器外,用户还可以根据需要自定义类加载器,通过继承java.lang.ClassLoader类并重写findClass()等方法来实现。自定义类加载器可以实现一些特殊需求,如加载加密的类文件、从网络上动态下载类等。
示例讲解
自定义类加载器通常通过继承java.lang.ClassLoader
类并重写findClass
方法来实现。
下面是一个简单的自定义类加载器示例:
import java.io.*;
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将类名转换为文件路径(假设类文件在classes目录下,并且是.class文件)
String fileName = name.replace(".", "/") + ".class";
// 从指定路径加载类文件
try (InputStream is = getClass().getClassLoader().getResourceAsStream(fileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
if (is == null) {
throw new ClassNotFoundException(name);
}
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
bos.write(buffer, 0, length);
}
byte[] classData = bos.toByteArray();
// 使用defineClass将字节数组转换为Class对象
return defineClass(name, classData, 0, classData.length);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
}
public static void main(String[] args) {
try {
MyClassLoader myClassLoader = new MyClassLoader();
Class<?> clazz = myClassLoader.loadClass("com.example.MyClass");
Object instance = clazz.getDeclaredConstructor().newInstance();
// ... 现在可以使用instance对象了
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:上述示例中,findClass
方法被重写以加载指定路径下的类文件。
然而,在实际应用中,更常见的是使用loadClass
方法加载类,因为loadClass
方法实现了双亲委派模型,首先会委托给父类加载器加载类,如果父类加载器加载不到,才会调用自己的findClass
方法。
直接使用findClass
方法可能会绕过双亲委派模型,这通常不是推荐的做法。