Java类加载
在Java编程中,类的加载是一个关键过程,它将字节码文件转换为运行时可以使用的对象。本文将详细介绍Java的类加载机制,并通过一个简单的示例进行演示。
1. Java类加载器
Java类加载器是JVM的一个重要组成部分,它的主要任务是从磁盘、网络或其他数据源读取字节码并将其转换为运行时可以使用的Java对象。Java的类加载器分为三个层次:Bootstrap ClassLoader(引导类加载器)、Extension ClassLoader(扩展类加载器)和AppClassLoader(应用程序类加载器)。
- Bootstrap ClassLoader:由C++实现,负责加载JDK的
lib目录下的核心类库,如rt.jar。 - Extension ClassLoader:加载
JAVA_HOME/lib/ext目录下的jar包或者java.ext.dirs系统属性指定的路径下的jar包。 - AppClassLoader:也称为SystemClassLoader,加载当前应用的ClassPath下的所有类。
2. 类加载过程
类的加载过程包括加载、验证、准备、解析和初始化五个阶段。
- 加载:读取二进制数据,创建Class对象。
- 验证:确保被加载的类信息符合JVM规范,不会危害系统安全。
- 准备:为类的静态变量分配内存,并初始化为默认值。
- 解析:将符号引用转为直接引用,即把类、接口、字段、方法的符号引用解析为直接引用。
- 初始化:执行类的初始化方法
<clinit>。
3. 示例代码
下面是一个简单的类加载器示例:
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String className) throws ClassNotFoundException {
try {
byte[] classBytes = loadClassData(className);
return defineClass(className, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(className, e);
}
}
private byte[] loadClassData(String className) throws IOException {
// 这里通常从文件、网络等获取class数据
String path = className.replace(".", "/") + ".class";
InputStream is = getClass().getResourceAsStream(path);
if (is == null) {
throw new FileNotFoundException("File not found: " + path);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int ch;
while ((ch = is.read()) != -1) {
baos.write(ch);
}
return baos.toByteArray();
}
}
这个例子定义了一个自定义类加载器,覆盖了loadClass方法,从当前类的资源路径加载类的数据。
4. 双亲委派模型
在Java中,类加载器遵循"双亲委派模型"。当一个类加载器接收到加载类的请求时,它首先会把这个请求委托给父加载器去完成,只有在父加载器无法加载这个类时,子加载器才会尝试自己去加载。这样设计可以避免类的重复加载,保证了同一个类只会被加载一次。 根据双亲委派模型,当一个类加载器加载一个类时,它会先委托其父类加载器去加载,如果父类加载器无法加载,则尝试自己加载。这样的委派机制保证了类的唯一性和一致性,同时也限制了类加载器之间的可见性。 在双亲委派模型下,子类加载器可以访问父类加载器加载的类,但是父类加载器无法访问子类加载器加载的类。这就导致了一种单向的可见性,即上层类加载器对下层类加载器的类可见,而下层类加载器对上层类加载器的类不可见。 这种限制保证了类加载器之间的隔离性,防止了类的混乱和冲突。同时,也确保了类加载器之间的独立性,每个类加载器加载的类都有自己的命名空间,不会受到其他类加载器的影响。
破环双亲委派模型
双亲委托模型(Parental Delegation Model)是Java类加载机制的一种,它的核心思想是:子类加载器在尝试加载类时,会先将请求委派给父类加载器。这样可以确保系统类(如java.*开头的类)由启动类加载器加载,而用户自定义类由应用类加载器加载,从而保证了系统的安全性和稳定性。
loadClass与findClass
loadClass(): 这是一个protected方法,由ClassLoader的子类实现。这个方法尝试加载指定名称的类。如果类已经加载,它就返回对应的Class对象。如果没有找到,那么它会调用findClass()方法来查找和加载类。loadClass()方法使用双亲委派模型,即先让父类加载器尝试加载,如果父类加载器无法加载,再由当前类加载器加载。
findClass(): 这个方法是由每个类加载器自己实现的,具体如何查找和加载类。在默认情况下,这个方法会在指定的目录下寻找.class文件,然后读取并生成Class对象。如果你想要自定义类加载的方式,比如从数据库或网络中加载类,你通常需要重写这个方法。
总的来说,loadClass()是高层接口,它是用户面对的,而findClass()则是实际进行加载操作的方法,是底层实现。在默认情况下,loadClass()会调用findClass()进行类的查找和加载,但用户可以通过覆盖loadClass()方法来改变这种行为。
破坏双亲委托模型demo
破坏双亲委托模型通常是不推荐的行为,因为这可能会导致类加载混乱和安全性问题。但如果确实有特殊需求,可以通过以下方式来实现:
- 自定义类加载器:创建一个新的类加载器,覆盖其
loadClass()方法,不在其中调用父类加载器的loadClass()方法,而是直接从特定位置加载类。
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 不使用父类加载器,自行加载类
return super.findSystemClass(name);
}
}
5. 命名空间隔离
在 Java 的类加载机制中,命名空间隔离是指不同的类加载器加载的类之间相互隔离,即使这些类具有相同的类名和包名。这种隔离性是通过类加载器的双亲委派模型来实现的。
由于不同的类加载器加载类的时候遵循双亲委派模型,同一个类在不同的类加载器中被加载后会形成不同的类定义,即使这些类具有相同的类名和包名。这就导致了命名空间隔离,即同一个类在不同的类加载器中被加载后,它们之间是相互隔离的,互相不可见,也就避免了类的冲突和混乱。
命名空间隔离在 Java 中有着重要的作用,它保证了不同组件、模块或应用之间的类不会相互干扰,确保了系统的稳定性和安全性。
6. 类的卸载
在 Java 中,类的卸载是指在运行时从内存中移除已加载的类。Java 虚拟机(JVM)具有自动的垃圾回收机制,其中包括对不再使用的类进行卸载的功能。类的卸载通常发生在以下情况下:
类加载器被卸载:当一个类加载器被回收时,它加载的所有类也会被卸载。这通常发生在应用程序重新加载或卸载模块时。
类不再被引用:当一个类不再被任何对象引用时,它会成为垃圾对象。在垃圾回收过程中,如果 JVM 发现某个类没有任何实例对象引用它,那么该类就会被卸载。
类的Class对象不再被引用