Java中的类加载器
在介绍双亲委派模型之前,首先先介绍下Java中的类加载器。
启动类加载器(Bootstrap ClassLoader)
首先先介绍下类加载器中的重量级器物,就是大名鼎鼎--启动类加载器。
为什么说他是重量级的恩,因为它是Java类加载器层次结构的最顶层,由虚拟机实现,用于加载Java核心类库,如java.lang和java.util等。
既然是最顶层的类加载器,我们就康康它有什么作用:
启动类加载器是由C/C++编写的,无法直接在Java代码中获取其引用。 它负责加载Java运行时环境所需的基本类。
扩展类加载器(Extension ClassLoader)
接着我们看一下Java加载器中的第二号人物--扩展类加载器。
扩展类加载器是由Java编写的,它是启动类加载器的子类。 它负责加载Java扩展类库,位于jre/lib/ext路径下的JAR文件。 扩展类加载器可以通过java.ext.dirs系统属性来指定其他目录作为扩展类库的路径。
应用程序类加载器(Application ClassLoader)
接着介绍最底层的加载器--应用程序类加载器。
应用程序类加载器是Java类加载器层次结构的最底层,也称为系统类加载器。 它负责加载应用程序类路径(Classpath)下的类,包括开发者自己编写的类和第三方库。 应用程序类加载器可以通过-classpath或-cp选项来指定加载类的路径。
自定义类加载器(Custom ClassLoader)
除了上述系统类的加载器,我们开发者还可以自定义加载器-惊不惊喜,意不意外。
自定义类加载器是开发者根据需求编写的自定义加载器,继承自ClassLoader类 。 它可以根据特定的加载规则和需求,从不同的来源加载类,比如本地文件系统、网络等。 自定义类加载器需要实现findClass()方法 ,指定类的加载规则,然后通过defineClass()方法加载类的字节码。
双亲委派模型
看到这里,上我们的重头菜,这块知识是面试中的重点内容。
双亲委派(Parent Delegation)是Java类加载机制中一种重要的实现方式,它通过一种递归的方式在类加载器之间建立了父子关系,并且定义了类加载的优先级。该模型主要用于解决类加载的冲突和隔离问题,保证Java应用程序的稳定性和安全性。
在Java类加载机制中,每个类加载器都有一个父加载器。当一个类加载器需要加载一个类时,它首先会委托给它的父加载器进行加载。只有当父加载器无法加载时,子加载器才会尝试加载。这种递归的委派模型可以形成一个类加载器的层次结构,称为类加载器树。
我看了《深入理解Java虚拟机-第三版》中的讲解,挺详细的,这里分享给大家:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法加载这个请求时,子加载器才会尝试自己去加载。
双亲委派模型源码
首先可以先尝试自己看下源码,非常通俗易懂:
java
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查类是否已经被加载
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
// 如果父类加载器存在,则委派给父类加载器
c = parent.loadClass(name, false);
} else {
// 否则,使用引导类加载器进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器也无法找到类,则尝试自己加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
在上面的源码中,我们可以看到双亲委派机制的体现。当loadClass方法被调用时,首先会检查该类是否已经被加载。如果没有加载,那么会按照以下步骤进行处理:
-
首先,调用findLoadedClass(name)方法检查类是否已经被其他类加载器加载过。如果是,则直接返回该类的Class对象。
-
如果这个类还未加载,则尝试委派给父类加载器加载。通过parent.loadClass(name, false)方法,将类加载任务传递给父类加载器。这个过程中,父类加载器会重复执行上述流程,直到达到引导类加载器为止。
-
如果所有的父类加载器都无法加载该类,那么会尝试使用启动类加载器加载。这是Java类加载器层次结构的最顶层。
-
如果引导类加载器仍然无法找到所需的类,就会调用findClass(name)方法在当前类加载器的作用域内查找并加载该类。
-
到最后,如果resolve参数为true,就会调用resolveClass(c)方法进一步解析和链接该类。
如何打破双亲委派模型
其实双亲委派模型并不是具有强制性约束的模型,虽然它有助于保证类的隔离和加载的顺序,但有时候我们可能需要打破这种机制,比如我们在特殊情况下需要自定义类加载逻辑或实现热部署的时候。
上周面试问道了这个问题,所以在这里也介绍几种打破Java中双亲委派机制的方法:
-
自定义类加载器:我们自定义一个继承自ClassLoader的类加载器,重写loadClass方法,可以实现自己的类加载逻辑。在我们定义的这个方法中,可以在需要时跳过父类加载器并直接加载类,从而打破双亲委派机制。但是,在自定义类加载器中打破双亲委派机制可能会导致类的隔离性和安全性问题,所以我们在使用时要慎重。
-
线程上下文类加载器:Java中提供了线程上下文类加载器(Thread Context ClassLoader)的概念,它可以在某些情况下打破双亲委派机制。例如,在JNDI、SPI(Service Provider Interface)和一些框架中,线程上下文类加载器可以用来加载指定的类,从而不受双亲委派机制的限制。
-
通过反射机制:使用反射机制可以直接调用Class类的defineClass方法,这样可以绕过双亲委派机制直接加载指定的类。但是,在使用这种方式时,我们需要自行处理类加载的顺序和依赖关系,因为双亲委派机制不再起作用。
综上所述,不正确地使用或滥用这些方法可能导致类加载冲突、安全问题以及代码的不稳定性 ,因此我们还需要在明确的需求和充分的理解下使用(面试会背诵就行【手动狗头】)。
文章到这里就先结束了,感兴趣的可以订阅专栏哈,后续会继续分享相关的知识点。