聊聊双亲委派机制
最近重新看了双亲委派相关的知识,特此记录一下,方便以后重新回顾
Java 类是怎么加载
Java 通过 ClassLoader 实例的 loadClass 方法将字节码(.class)文件加载到 JVM 的方法区中。啥也不说,先上代码 (~ ̄▽ ̄)~:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 从已加载的类中寻找
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
//父委派机制
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//没有父类加载器,则说明父加载器是启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//根据类名从数据源查找对应的字节码,读取二进制流,生成 Class 对象
c = findClass(name);
}
}
if (resolve) {
//官方注释说是链接一个类,这里暂时没用过,下次看看
resolveClass(c);
}
return c;
}
}
//此处贴的是 ClassLoader 子类 URLClassLoader 的 findClass 方法,也是默认的实现
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
//根据类名从资源路径查找对应的资源
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//读取二进制流,生成 Class 对象
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
从上面的代码可以看到 loadClass 主要做了两件事情:
- 根据类名从资源路径(文件、网络等)查找对应的字节码资源
- 从资源路径读取字节码二进制流,并生成对应的 Class 对象,其中包括权限校验,字节码校验等。
Java 默认的类加载器
JDK9 之前默认的类加载器有:AppClassLoader、ExtClassLoader、BootstrapClassloader
- BootstrapClassLoader: JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类),注意,他不是定义在 Java 代码中的。
- ExtClassLoader:存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。
- AppClassLoader:负责加载应用程序路径下的类(虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径)
JDK9 及以后默认的类加载器:AppClassLoader、PlatformClassLoader、BootstrapClassLoader
- PlatformClassLoader:其实就是之前的 ExtClassLoader,只是将大部分原本由 BootstrapClassLoader 加载的类交由 PlatformClassLoader 加载
Java 类的加载时机有哪些
类加载的时机有很多,这里主要列举一些易错场景 (~ ̄▽ ̄)~:
public class ClassLoadTimingTest {
public static void main(String[] args) throws ClassNotFoundException {
//new一个对象实例
new A(); //加载A
//通过反射实例化一个对象
Class.forName("com.classloader.demo.char01.A"); //加载A
//访问类的静态变量
System.out.println(A.word); //加载A
//访问类的静态方法
A.println(); //加载A
//子类初始化会触发父类初始化
System.out.println(B.word1); //加载A、B
//以下情况不加载
System.out.println(B.word); //加载A,不加载B
//创建数组不加载类
System.out.println(new A[]{}); //不加载A
//访问常量不加载类
System.out.println(A.DEFAULT); //不加载A
//易混淆情况,编译器无法确定最终变量的值,所以运行期要去加载
System.out.println(A.DEFAULT1); //加载A
}
}
class A{
protected final static String DEFAULT = "HELLO WORLD";
protected final static String DEFAULT1 = new String("123");
public static String word = "HELLO WORLD";
static {
System.out.println("A was loader");
}
public static void println(){}
}
class B extends A{
public static String word1 = "HELLO WORLD";
static {
System.out.println("B was loader");
}
}
什么是双亲委派机制
仔细的同学应该会发现,上面加载类的方法中,优先会调用 parent.loadClass 去加载类,如果没加载到才会走继续往下走。
其实这就是类加载的父委派机制,优先由父加载器去加载类,父类加载器没加载到才有自身尝试去加载。
那么什么是双亲委派机制呢,还记得上面提到 JDK8 的默认类加载器有 AppClassLoader、ExtClassLoader、BootstrapClassloader,BootstrapClassloader 是 ExtClassLoader 的父加载器,ExtClassLoader 是 AppClassLoader 的父加载器。嗯,就是因为 AppClassLoader 有一个爸爸和一个爷爷,所以称为双亲委派机制。就是这么简单 ︿( ̄︶ ̄)︿。
那么双亲委派机制的作用是什么呢,我认为有两点:
- 防止同一个类被加载到 JVM 多次
- 避免 Java 内部的一些基础类没有被正确加载,导致出现难以意料的异常
JDBC 真的打破双亲委派了吗
在说 JDBC 是否打破双亲委派之前我们先来聊聊什么是打破双亲委派机制,嗯,就是让类的加载顺序不在是 BootstrapClassloader -> ExtClassLoader -> AppClassLoader,就是字面上的意思不再让爷爷和爸爸先加载,而是儿子自己想先加载就先加载 ︿( ̄︶ ̄)︿。那么我们该怎么做呢,是不是自定义一个类加载器,重写下 loadClass 方法不在调用 parent.loadClass 就可以了?嗯,是的 o( ̄▽ ̄)d。
那么我们再来看看 JDBC 真的打破双亲委派了吗。老规矩,线上代码 (~ ̄▽ ̄)~:
AppClassLoaderpublic class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
//获取环境变量中配置的驱动
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//通过SPI机制加载驱动类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
*/
try{
while(driversIterator.hasNext()) {
//这里面会加载驱动类(不初始化),但是会通过反射实例化驱动类(会初始化)
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
//加载驱动类并初始化
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
}
public final class ServiceLoader<S> {
private final ClassLoader loader;
private LazyIterator lookupIterator;
public static <S> ServiceLoader<S> load(Class<S> service) {
//拿到线程上下文中的类加载器,这里如果未设置过获取到的是 AppClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}
从上面的代码可以看到,DriverManager 通过 SPI 机制,加载 Driver 驱动类,而 ServiceLoader 中其实只是拿到当前上下文中的类加载器,那么能说他打破了双亲委派机制吗?我个人觉得是不能的,先不说其他,在未手动设置线程上下文中的类加载器的情况下,线程上下文类加载器是继承至父线程的,其实也就是 AppClassLoader,那正常情况下他走的就是双亲委派机制 ( ̄︶ ̄)↗。就算是将自定义的类加载器放入线程上下文中,那也是由自定义的类加载器通过重写 loadClass 打破双亲委派机制呀,所以我认为 JDBC 自身并未打破了双亲委派机制 <( ̄︶ ̄)>。
Tomcat 如何打破双亲委派
那么 Tomcat 打破了吗?嗯,它打破了 b( ̄▽ ̄)d,Tomcat7 及以上自定义了类加载器 ParallelWebappClassLoader 和 WebappClassLoader。嗯,这里注意一下,Tomcat6 以下的类加载器有所不同,不过原理都是一样的(其实是我偷懒,不想把 Tomcat6 源码也看了 (╯▽╰))。
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (JreCompat.isGraalAvailable() ? this : getClassLoadingLock(name)) {
String resourceName = binaryNameToPath(name, false);
//这里获取到 ExtClassLoader
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
//先ExtClassLoader和BoostrapClassLoader的资源路径中查找字节码资源
url = javaseLoader.getResource(resourceName);
}
//如果资源存在则走ExtClassLoader和BoostrapClassLoader加载
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
//如果资源存在则走ExtClassLoader和BoostrapClassLoader加载
clazz = javaseLoader.loadClass(name);
//return
} catch (ClassNotFoundException e) {
// Ignore
}
}
//判断是否需要委派给父类加载器,默认是AppClassLoader,或者是Tomcat内部的一些类
boolean delegateLoad = delegate || filter(name, true);
// (1) Delegate to our parent if requested
if (delegateLoad) {
try {
clazz = Class.forName(name, false, parent);
//return
} catch (ClassNotFoundException e) {
// Ignore
}
}
try {
//从本地目录中查找类并加载
clazz = findClass(name);
//return
} catch (ClassNotFoundException e) {
// Ignore
}
// (3) Delegate to parent unconditionally
if (!delegateLoad) {
try {
//还是没有找到就委派给父加载器,默认是AppClassLoader
clazz = Class.forName(name, false, parent);
//return
} catch (ClassNotFoundException e) {
// Ignore
}
}
}
throw new ClassNotFoundException(name);
}
我们看看 Tomcat 都改了啥:
- 首先尝试通过 ExtClassLoader 加载类,防止 java 相关的一些类被先加载了
- 判断是否需要委派给父类加载器,或者是是否是Tomcat内部的一些类,是的话则委派给父类加载器
- 从本地目录和扩展路径中查找类并加载,还记的早起我们使用 Tomcat 时,会将多个 war 包放在 webapps 吗,那么如果多个 war 中完全的全路径类名,走默认的双亲委派机制就只有先加载的能加载成功。此处便是 Tomcat 打破双亲委派意义了。
- 如果还是没有找到,就委派给父类加载器,走双亲委派机制。
可以看到,Tomcat 的类加载顺序不再是 BootstrapClassloader -> ExtClassLoader -> AppClassLoader,而是在 AppClassLoader 前先尝试通过 WebappClassLoader 加载本地目录 b( ̄▽ ̄)d。
自定义类加载器
篇幅有点长了,这个下次一定 ╰( ̄▽ ̄)╭
总结
- Java 通过 ClassLoader 实例的 loadClass 方法将字节码(.class)文件加载到 JVM 的方法区中。
- JDK9 之前默认的类加载器有:AppClassLoader、ExtClassLoader、BootstrapClassloader ,JDK9 及以后默认的类加载器:**AppClassLoader、PlatformClassLoader、BootstrapClassLoader **。
- 类加载时机包括:new一个对象实例;反射实例化一个对象;访问类的静态变量、方法;子类初始化会触发父类初始化;方法句柄调用加载方法所在类。
- 不加载时机:访问父类静态变量,不加载子类;创建数组不加载类;访问常量不加载类,但是如果编译是无法确定,运行期还是会加载类。
- JVM 为防止同一个类被加载多次,引入双亲委派机制,加载类过程中,先由其父类加载器加载,未加载到在自己加载
- JDBC 并未打破双亲委派,而 Tomcat 通过自定义 ClassLoader 打破双亲委派。
转载自:https://juejin.cn/post/7227486312340537401