最近面试被问到双亲委派,对于双亲委派和破坏双亲委派的机制以前本身在《深刻理解Java虚拟机》中了解过,当时以为挺简单的一个概念,可是面试官仔细追问下去发现本身这块的只是仍是存在一些误区,当时这一个问题可能聊了有20分钟,固然面试后天然就没有下文了😂。java
故事要从这张双亲委派模型的类UML图提及: 面试
findClass()
与loadClass()
之间的关系和这两个方法具体的做用,破坏双亲委派机制与遵循双亲委派机制与这两个方法的相关性;ClassLoader
,没有理清Java中ClassLoader
类与Bootstrap Class Loder
、Extension Class Loader
、Application Class Loader
之间的关系;错误的按照类加载器模型图,认为本身实现类加载器须要继承应用程序类加载器以实现自定义类加载器。误区造成的最主要缘由是当时本身看《深刻理解Java虚拟机》的时候,其实仍是处于知识储备、知识之间的关联性还不够到位的状态,所以今天从新进行梳理一下~数据库
什么是双亲委派模型? 双亲委派模型定义了加载器加载类的方式,即当一个加载器收到加载一个类的请求时,首先会委托给父加载器进行处理加载,只有当其父加载器没法加载时,当前加载器才会进行加载;缓存
那么何时会触发类加载器进行类的加载呢? 当在程序中出现对于一个类的主动引用时,若是当前类还没有被加载到方法区中时,会触发对引用类的加载;安全
触发类的加载后类加载器须要作些什么? 虚拟机经过类加载器在Java类加载阶段须要完成如下三件事:markdown
java.lang.Class
对象,做为方法区这个类的各类数据的访问入口。那么何时父加载器没法加载一个类呢? 由于每一个加载器加载负责加载的类都是不一样的:数据结构
<JAVA_HOME>\lib
目录下的类,并经过文件名匹配,如rt.jar
、tools.jar
这类Java核心类库;这个类经过C++
语言进行实现,是虚拟机的一部分;<JAVA_HOME>\lib\ext
目录下的扩展类;经过Java
语言进行实现,对应的类为sun.misc.Launcher$ExtClassLoader
;classpath
)下的全部类库;经过Java
语言进行实现,对应的类为sun.misc.Launcher$AppClassLoader
,能够经过ClassLoader
类中的getSystemClassLoader()
方法获取应用程序类加载器的引用,而且应用程序中若是没有自定义类加载器,则会使用这个默认的类加载器;JDK1.2
以后,经过ClassLoader::loadClass()
实现双亲委派模型,而且咱们使用ClassLoader时通常也会经过loadClass()
方法进行类的加载:app
public abstract class ClassLoader {
// 当前ClassLoader依赖的父加载器,注意父加载器与当前加载器是组合的关系!
private final ClassLoader parent;
// 在构建classLoader时,默认会将系统类加载器AppClassLoader做为当前classLoader的父加载器
// getSystemClassLoader()默认返回AppClassLoader
// 而AppClassLoader的父加载器也是在初始化时设置为 PlatformClassLoader
protected ClassLoader() {
this(checkCreateClassLoader(), null, getSystemClassLoader());
}
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 1. 首先检查是否已经加载过当前类
Class<?> c = findLoadedClass(name);
// 2. 未加载过当前类则进行加载
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 2.1 父加载器不为空,则先使用父加载器进行加载
c = parent.loadClass(name, false);
} else {
// 2.2 父加载器为空,则使用启动类加载器进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ignore未找到当前类的异常,说明父加载器加载失败
}
if (c == null) {
long t1 = System.nanoTime();
// 2.3 经过当前类加载器进行加载,注意此处是调用了findClass()方法
// 而不是像父类加载器经过loadClass()进行加载
// 所以若是要遵循双亲委派机制,子加载器须要经过覆盖findClass()方法实现本身的加载逻辑
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
复制代码
主要缘由在于经过双亲委派模型组织类加载器之间的关系,使得在Java中的类随着它的类加载器也具有了一种带优先级的层次关系,而这种关系保证了类加载的安全性;maven
如用户本身定义了一个Object
类,那么按照双亲委派模型,首先会经过启动类加载器进行Object
类的加载,则天然会加载在<JAVA_HOME>\lib
目录下的Obejct.Class
类,而不会加载用户在自身classpath下定义的Object类,从而保证了Java核心类库的类不会被错误的加载。ide
遗憾的是,双亲委派模型在面对复杂的世界时,并不能完美的解决全部问题:
比较常见的是JDBC
,由于咱们项目使用的数据库多是Oracle
、MySQL
、SQL Server
等,而不一样的数据库由于使用方式的不一样提供了不一样的JDBC Driver
,从而使得应用代码能够经过调用JDBC Driver Interface
对不一样数据进行增删改查操做;
Connection connection = DriverManager.getConnection(jdbcUrl, username, password);
复制代码
可是用于获取数据库链接的DriverManager
类是位于rt.jar
下的,按照双亲委派模型理应经过启动类加载器进行加载,可是这个类又会由于项目使用的数据库类型依赖不一样的第三方实现的Driver
类,而第三方的是实现类通常是放在用户类路径(classpath)下的,启动类加载器天然没法加载;
JDK1.6
经过引入基于ThreadContextClassLoader
(线程上下文类加载器)实现的ServiceLoader
这个特殊的类加载器用来解决这个问题:
public static <S> ServiceLoader<S> load(Class<S> service) {
// 线程上下文类加载器默认为AppClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 经过AppClassLoader加载上层组件中依赖的用户classpath下的包
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
复制代码
可是线程上下文使用不当可能会致使内存泄漏的问题,具体能够参考这篇文章:线程上下文类加载器ContextClassLoader内存泄漏隐患
对于在一个JVM上会运行多个Web容器的Tomcat,类可见性若是处理不够稳当,会致使Web容器间缺少合理的隔离性从而带来安全性和容器间依赖组件版本冲突以及没法支持类热更的问题;
安全性问题: 按照双亲委派机制,对于同一个JVM下CLASSPATH
指定路径下的全部类和库,都会经过AppClassLoader
进行加载,所以CLASSPATH
路径下的类能够被全部运行在JVM上的Web容器经过AppClassLoader
类加载器进行加载、使用;可是对于运行在JVM上的每一个Web应用(servlet容器),容器中的servlet应该只能够访问自身WEB-INF/classes
和WEB-INF/lib
目录下的类;而若是经过AppClassLoader
类加载器加载servlet,那么servlet即可以经过AppClassLoader
访问到CLASSPATH
下的其余非本容器的类,而这显然违反了servlet容器须要的类隔离性;
版本冲突: 项目的多个模块间可能存在对于同一个第三方组件不一样版本的依赖,而按照双亲委派模型,若是使用AppClassLoader
做为类加载器加载各个模块的依赖类,会致使在CLASSPATH
路径下只有一个版本的第三方组件会被加载;PS:相似的还有经过maven直接或间接引入了一个包的多个版本致使版本冲突的问题,也是经过自定义类加载器模型进行解决;
没法支持热更: 由于按照双亲委派机制,不一样模块之间共同依赖了同一个AppClassLoader
,使得模块之间对于所加载的类存在耦合关系,即咱们不能够随便卸载并替换一个类,由于并不知道是否还有其余模块依赖这个版本的第三方组件;
为了解决容器间缺少隔离性带来的一系列问题,Tomcat自定义了自身的类加载器模型:
Tomcat容器经过复写ClassLoader::loadClass()
方法自定义了破坏双亲委派模型的WebappClassLoader
类加载器,并为每一个Web应用都关联了一个WebAppClassLoader
,从而实现了Web应用间特殊依赖类的隔离性,即每一个应用首先会经过自身该类加载器加载自身WEB-INF/classes
、WEB-INF/lib
目录下依赖的类库, 只有当加载不到所须要的类时才会交由上层类加载器进行加载(破坏了双亲委派首先交由父类进行加载的规则);
可是Tomcat的每一个Web应用间可能也会存在共性的一些依赖,如Servlet规范相关包以及一些工具类包,所以Tomcat的类加载器模型经过父加载器SharedClassLoader
进行加载在Web应用间共享的jar包。
而按照该类加载器模型,实现热更只须要动态替换掉Web容器的WebAppClassLoader
便可,避免了重启整个Java项目带来的损耗,并不须要担忧被替换掉的类加载器所加载的类会不会被运行在同一个JVM上的Web应用引用的问题。
主要在复写的loadClass()
方法中进行自身加载模型的定义,Tomcat自定义类加载器除了上述中定义类加载器模型的目的以外,还有实现对于已加载类的缓存、类的预载入这两个方面;
public abstract class WebappClassLoaderBase extends URLClassLoader {
// ClassLoader对于已加载类的内存缓存
protected final Map<String, ResourceEntry> resourceEntries = new ConcurrentHashMap<>();
@Override
// @param: name,须要加载的类的全限定名
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 1. 缓存查询
// 1.1 WebAppClassLoader自身Map缓存
clazz = findLoadedClass0(name);
if (clazz != null) {
return clazz;
}
// 1.2 查询JVM缓存
clazz = findLoadedClass(name);
if (clazz != null) {
return clazz;
}
// 2. 开始类加载
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
// 2.1 首先尝试使用AppClassLoader的父加载器进行加载
// 主要是为了不Tomcat类目下自定义类覆盖JavaSE的核心类
URL url = javaseLoader.getResource(resourceName);
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 将加载类的全限定名传入filter()方法判断是否须要委托给父加载器进行加载
boolean delegateLoad = delegate || filter(name, true);
// 2.2 若是须要进行委托加载,则交由AppClassLoader进行类的加载
// 而AppClassLoader是遵循双亲委派模型的
if (delegateLoad) {
try {
// 交由父类AppClassLoader进行加载
clazz = Class.forName(name, false, parent);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 2.2 不须要进行委托,则破坏双亲委派机制,首先经过WebAppClassLoader加载器加载
// 具体的类加载方法由自身的findClass()方法进行实现
// 这里能够看到,loadClass()方法通常用于实现类加载器模型,
// 而由findClass()方法负责实现具体的类加载手段
try {
clazz = findClass(name);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 2.3 若是当前Web应用Class目录下不存在当前须要加载的类
// 则从新按照双亲委派模型使用父加载器进行最终的加载尝试
if (!delegateLoad) {
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
// 2.4 未能成功加载所须要的类,抛出ClassNotFound异常
throw new ClassNotFoundException(name);
}
}
复制代码
ClassLoader::loadClass()
方法经过组合父加载器实现优先父加载器加载的模型;loadClass()
通常用于实现类加载器模型,诸如双亲委派模型,Tomcat类加载器模型等;而findClass()
则用于在定义当前类加载器模型下当前类加载器具体的加载类手段;findClass()
方法便可;若是须要破坏双亲委派模型,则须要复写loadClass()
与findClass()
。主要参考:
《深刻剖析Tomcat》 《深刻理解Java虚拟机 Edition3》