类加载器ClassLoader
1.双亲委派模型
java是根据双亲委派模型的加载类的,当一个类加载器加载类时,会先尝试委托给父类加载器去加载,直到到达启动类加载器顶层若加载不了,则再让子类加载器去加载直到类成功加载,否则抛出异常。
双亲委派模型的好处是可以保证类加载的安全性,无论加载哪个类都会向上委托给BootstrapClassLoader类加载器,BootstrapClassLoader加载不了再尝试自己加载,这样就可以保证用不同的类加载器加载可以得到同一个class对象
2. 双亲委派模型的缺陷
依赖双亲委派模型,子类加载器可以使用父类加载器加载的类,而父类加载器无法使用子类加载器加载的类,这是因为子类加载器可以向上委托让父类加载器加载器,而父类加载器无法向下让子类加载器去加载类。这就导致了双亲委派模型并不是能处理所有类加载的场景,它有自己的局限。
另外类加载器有命名空间的概念,一个类加载器的命名空间由这个类加载器加载的类和其父类加载器加载的类组成,并且同一命名不存在全限定名相同的两个类对象,同一命名空间的类是相互可见的,不同命名空间的类不能相互访问。
接下来针对子类加载器可以使用父类加载器加载的类,而父类加载器无法使用子类加载器加载的类这句话写个简单的例子来证明
3. 用例
首先我们自定义一个类加载器MyClassLoader,它可以加载path路径下的class文件。为了不破坏双亲委派模型,MyClassLoader类没有重写loadClass方法只重写了findClass方法,当父类加载器无法加载指定类的时候,MyClassLoader会调用findClass方法去尝试加载类
以下的类都定义在package包loader下面
public class MyClassLoader extends ClassLoader {
private String path;
MyClassLoader() {
this.path = Objects.requireNonNull(MyClassLoader.class.getResource("/")).getPath();
}
MyClassLoader(String path) {
this.path = path;
}
@Override
public Class<?> findClass(String name) {
try {
byte[] bytes = readBytes(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
return null;
}
}
private byte[] readBytes(String name) throws IOException {
String realPath = path + "/" + name.replace(".", "/") + ".class";
System.out.println("自定义加载器加载类" + name + ", class文件路径:" + realPath);
try (FileInputStream fileInputStream = new FileInputStream(realPath);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] bytes = new byte[1024];
while (true) {
int len = fileInputStream.read(bytes);
if (len <= 0) {
break;
}
byteArrayOutputStream.write(bytes, 0 , len);
}
return byteArrayOutputStream.toByteArray();
}
}
}
测试代码,下面各个场景都是一样的,定义了myClassLoader类加载器,可以加载路径/Users/xudong下的class文件。因为MyClassLoader由AppClassLoader加载所有它的父类加载器是AppClassLoader
public class ClassC {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("/Users/xudong");
// 让类初始化
Class<?> classA = Class.forName("loader.ClassA", true, myClassLoader);
Class<?> classB = Class.forName("loader.ClassB", true, myClassLoader);
System.out.println("classA's classLoader:" + classA.getClassLoader());
System.out.println("classB's classLoader:" + classB.getClassLoader());
}
}
场景一
定义两个类ClassA和ClassB,之后将ClassA.class和ClassB.class复制到/Users/xudong/loader目录下
public class ClassA {
}
public class ClassB {
}
运行测试代码可以看到ClassA和ClassB都由AppClassLoader加载,虽然一开始由myClassLoader加载但根据双亲委派模型会委托给父类加载器加载,父类加载器加载不了再由子类加载。项目classpath和/Users/xudong下都有class文件但AppClassLoader是myClassLoader的父类加载器,因此先由AppClassLoader加载成功返回。
场景二
在场景一的基础上,删除classpath路径下的ClassB.class文件,这样的话只有/Users/xudong下有ClassB.class文件了,接着运行测试代码可以看到ClassB类由myClassLoader加载,因为AppClassLoader加载器在classpath下找不到ClassB.class文件,所以由myClassLoader尝试下加载
场景三
修改ClassB.java重新编译,接着将classpath下的ClassA.class和ClassB.class复制到/Users/xudong/loader路径下,删除classpath下的ClassB.class
public class ClassB {
static {
ClassA classA = new ClassA();
System.out.println("ClassB 静态代码块执行:" + classA.getClass().getClassLoader());
}
}
运行测试代码可以看到ClassB由MyClassLoader类加载器加载符合场景二的结果,在类加载的时候进行类初始化执行静态代码块,实例化ClassA对象(肯定需要先加载ClassA类),因为ClassB是MyClassLoader加载的所以ClassA也会用MyClassLoader加载,向上委托,因为classpath存在ClassA.class所以会由AppClassLoader加载器加载,这也说明了子类加载器可以使用父类加载器加载的类。
若把classpath下的ClassA.class也删除呢,那么程序不会报错,只是ClassA类由MyClassLoader加载,因为已经加载了ClassA,所以在ClassB实例化的时候不会再加载ClassA:
场景四
修改ClassA.java和ClassB.java文件,重新编译,将ClassA.class和ClassB.class复制到/Users/xudong/loader路径下并将classpath下的ClassB.class文件删除
public class ClassA {
static {
ClassB classB = new ClassB();
System.out.println("ClassA 静态代码块执行:" + classB.getClass().getClassLoader());
}
}
public class ClassB {
}
执行测试代码报错了找不到ClassB,根据前面的场景我们知道ClassA由AppClassLoader加载,接着运行static静态代码块,由AppClassLoader加载ClassB,但classpath下并没有ClassB.class所以加载失败,在这种情况下需要AppClassLoader的子类MyClassLoader才能加载ClassB,但由于双亲委派模型我们知道不知道向下委托加载,这也说明了父类加载器并不能使用子类加载器加载的类。ClassB不在AppClassLoader类加载器的命名空间里
最后如有写错的地方,欢迎指正~