likes
comments
collection
share

聊聊双亲委派机制

作者站长头像
站长
· 阅读数 17

最近重新看了双亲委派相关的知识,特此记录一下,方便以后重新回顾

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 主要做了两件事情:

  1. 根据类名从资源路径(文件、网络等)查找对应的字节码资源
  2. 从资源路径读取字节码二进制流,并生成对应的 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 有一个爸爸和一个爷爷,所以称为双亲委派机制。就是这么简单 ︿( ̄︶ ̄)︿。

那么双亲委派机制的作用是什么呢,我认为有两点:

  1. 防止同一个类被加载到 JVM 多次
  2. 避免 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 都改了啥:

  1. 首先尝试通过 ExtClassLoader 加载类,防止 java 相关的一些类被先加载了
  2. 判断是否需要委派给父类加载器,或者是是否是Tomcat内部的一些类,是的话则委派给父类加载器
  3. 从本地目录和扩展路径中查找类并加载,还记的早起我们使用 Tomcat 时,会将多个 war 包放在 webapps 吗,那么如果多个 war 中完全的全路径类名,走默认的双亲委派机制就只有先加载的能加载成功。此处便是 Tomcat 打破双亲委派意义了。
  4. 如果还是没有找到,就委派给父类加载器,走双亲委派机制。

可以看到,Tomcat 的类加载顺序不再是 BootstrapClassloader -> ExtClassLoader -> AppClassLoader,而是在 AppClassLoader 前先尝试通过 WebappClassLoader 加载本地目录 b( ̄▽ ̄)d。

自定义类加载器

篇幅有点长了,这个下次一定 ╰( ̄▽ ̄)╭

总结

  1. Java 通过 ClassLoader 实例的 loadClass 方法将字节码(.class)文件加载到 JVM 的方法区中。
  2. JDK9 之前默认的类加载器有:AppClassLoader、ExtClassLoader、BootstrapClassloader ,JDK9 及以后默认的类加载器:**AppClassLoader、PlatformClassLoader、BootstrapClassLoader **。
  3. 类加载时机包括:new一个对象实例;反射实例化一个对象;访问类的静态变量、方法;子类初始化会触发父类初始化;方法句柄调用加载方法所在类。
  4. 不加载时机:访问父类静态变量,不加载子类;创建数组不加载类;访问常量不加载类,但是如果编译是无法确定,运行期还是会加载类。
  5. JVM 为防止同一个类被加载多次,引入双亲委派机制,加载类过程中,先由其父类加载器加载,未加载到在自己加载
  6. JDBC 并未打破双亲委派,而 Tomcat 通过自定义 ClassLoader 打破双亲委派