1. Java类加载器类型
Java当中类加载器分为3种:
- 启动类加载器:在Hotspot VM中由C++实现
- 扩展类加载器:由Java实现,继承自ClassLoader
- 应用程序类加载器:由Java实现,继承自ClassLoader
代码打印下3种类加载器的类加载路径:
public class A {
public static void main(String[] args) {
System.out.println("Bootstrap ClassLoader加载范围:");
URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
Stream.of(urLs).forEach(System.out::println);
System.out.println("==============================");
URLClassLoader extClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader().getParent();
System.out.println(extClassLoader);
System.out.println("Extension ClassLoader加载范围:");
urLs = extClassLoader.getURLs();
Stream.of(urLs).forEach(System.out::println);
System.out.println("==============================");
URLClassLoader appClassLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
System.out.println(appClassLoader);
System.out.println("Application ClassLoader加载范围:");
urLs = appClassLoader.getURLs();
Stream.of(urLs).forEach(System.out::println);
}
}
运行结果:

2. 双亲委派模型

双亲委派模型是:当一个类加载器收到类加载请求时,首先它不会自己去尝试加载这个类,而是把这个类加载请求委派给它的父类加载器去加载,每个加载器都是如此,一直到启动类加载器;当父类加载器在自己的搜索范围内找不到指定要加载的类时(ClassNotFoundException),子加载器才尝试自己去加载。
注意,类加载器的这种父子关系不是通过继承关系实现的,而是通过组合关系实现的,这种组合关系是通过ClassLoader类中的parent属性体现的,具体见8.类加载器组合关系体现
public abstract class ClassLoader {
...
private final ClassLoader parent;
...
}
3. 源码解释双亲委派模型
首先给出牵涉到的方法的流程图:

loadClass:

findClass:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
defineClass:
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError{
return defineClass(name, b, off, len, null);
}
一般在findClass中调用defineClass,defineClass是JDK提供的将字节数组转化成Class对象。findClass找到要加载的类的.class文件,并读取成字节数组,然后defineClass将字节数组转换成Class对象。
4. 自定义类加载器
根据3中对双亲委派模型源码的分析,发现自定义类加载器只需要覆写findClass即可。
package mustang.horse;
public class A {
public void hello() {
System.out.println("hello");
}
}
将A.java编译生成的class文件放在D盘中,然后删除项目中的A.java、A.class,防止因为双亲委派由AppClassLoader加载A类。
public class Main {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadBytes(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadBytes(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
int length = fileInputStream.available();
byte[] bytes = new byte[length];
fileInputStream.read(bytes);
fileInputStream.close();
return bytes;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:");
Class<?> clazz = myClassLoader.loadClass("mustang.horse.A");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("hello", null);
method.invoke(obj, null);
System.out.println(obj.getClass().getClassLoader());
}
}
输出结果:

5. 为什么使用双亲委派模型?
使用双亲委派的原因:
- 避免用户编写的类动态替换Java的核心类。比如用户编写了一个类java.lang.Object,要加载时,按照双亲委派模型,最终启动类加载器会在自己的类加载路径下搜索到java.lang.Object,而不是用户定义的Object,避免了java核心类被用户替换。(其实,自定义类加载器打破双亲委派模型后,也不能加载自己写的Object类替换java核心类库中的Object,因为JVM对java.*开头的类做了限制,保证必须由Bootstrap类加载器加载,具体见下图)
- 避免了类的重复加载。相同的类被不同的ClassLoader加载就是不同的两个类,按照双亲委派模型,指定类只会被一个类加载器加载,不会重复加载。

6. 打破双亲委派模型
根据对双亲委派模型源码的分析,打破双亲委派模型,只需要在自定义类加载器时重写loadClass即可。
public class Main {
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadBytes(name);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
private byte[] loadBytes(String name) throws IOException {
name = name.replaceAll("\\.", "/");
FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class");
int length = fileInputStream.available();
byte[] bytes = new byte[length];
fileInputStream.read(bytes);
fileInputStream.close();
return bytes;
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D:");
Class<?> clazz = myClassLoader.loadClass("mustang.horse.A");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("hello", null);
method.invoke(obj, null);
System.out.println(obj.getClass().getClassLoader());
}
}
运行结果:

发现打破双亲委派模型后,Main的父类Object也需要自定义的累加器加载,但是在该自定义累加器的加载路径中并没有Object这个类。
7. 线程上下文类加载器
类A依赖类B,在类A加载的时候,如果类B没有加载,那么类A会加载自己的类加载器去加载类B。
如果类A需要使用Bootstrap类加载器加载(存在于java核心包中的类),类B需要使用App类加载器加载,类A依赖类B,那怎么办呢?
JDK1.2 开始引入线程上下文类加载器:
在上面的情景中,类A加载时就可以拿到当前线程上下文类加载器去加载类B。(如果这个线程上下文类加载器恰好是App类加载器的话)
Thread.currentThread().getContextClassLoader();
新建线程如果不设置上下文类加载器的话,默认会继承父线程上下文类加载器。默认情况下,线程上下文类加载器就是App ClassLoader。也可以通过下面方法设置。
Thread.currentThread().setContextClassLoader(ClassLoader cl);
线程上下文类加载器也是打破双亲委派模型的一种方式,因为在双亲委派模型中父类加载器是无法委派给子类加载器的。
8. 类加载器组合关系体现
首先看ExtClassLoader、AppClassLoader类关系图:

ExtClassLoader构造函数:ExtClassLoader在构造时,其parent属性指定为null。

AppClassLoader构造函数:AppClassLoader在构造时,其parent属性会通过构造函数传入。

再来看Launcher的构造方法:

发现,先构造出ExtClassLoader,再构造AppClassLoader,并且将ExtClassLoader设置为AppClassLoader的父类加载器。
本文围绕Java类加载器展开,介绍了启动、扩展、应用程序3种类加载器。阐述双亲委派模型,即类加载请求先委派给父类加载器。通过源码解释该模型,还说明了自定义类加载器、使用该模型的原因、打破方式,以及线程上下文类加载器和类加载器组合关系。

287

被折叠的 条评论
为什么被折叠?



