JVM类加载机制解析(二)

776 阅读7分钟

JVM类加载机制解析(二)

双亲委派模型

1. 双亲委派模型的概念及原理

上文解释了Java中的四种类加载器,那么JVM具体如何确定一个类该由哪个类加载器进行加载的呢?此时就该引入双亲委派模型 。 上文提到的四种类加载器:

  • Bootstrap Class Loader 引导类加载器
  • Extension Class Loader 扩展类加载器
  • Application Class Loader 应用程序类加载器
  • User Class Loader 自定义类加载器

这几个类加载器层级关系如下图所示:

image.png 这种展示了类加载器之前层级关系的模型就称为双亲委派模型。该模型要求除了引导(启动)类加载器之外,其他的类加载器必须拥有父级类加载器。并且各个类加载器之间父子级关系并不是通过我们常用的继承关系来实现的,而是通过组合关系实现。

双亲委派模型的原理就是:当类加载器要去加载某个类的时候,这个类加载器会先委派父类加载器去加载这个类,父类加载器也是同理直到这个加载请求被委派到引导(启动)类加载器为止。当且仅当父类加载器无法加载这个类的时候,当前的类加载器才会去加载这个类。

现在我们来从代码的角度来分析双亲委派模型是如何实现的:

// ClassLoader中的loadClass方法实现了双亲委派模型
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
   	synchronized (getClassLoadingLock(name)) {
        // 查看当前是否已经加载了给定类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 判断当前类加载器的父类加载器是否为空
                if (parent != null) {
                    // 如果不为空则让父类加载器去加载给定类
                    c = parent.loadClass(name, false);
                } else {
                    // 如果为空则让引导(启动)类加载器去加载给定类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载器如果无法加载给定类则会抛出ClassNotFoundExcetion
            }
			
            if (c == null) {
                // 如果类为空表示父类无法加载给定类,此时就需要自己去加载给定类
                long t1 = System.nanoTime();
                // 类加载器自己加载给定类
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

2. 双亲委派模型的作用

双亲委派模型的设计保护了核心API类,比如我们自己写了一个java.lang.Object类,当应用程序类加载器加载这个类的时候,最终会委派引导(启动)类加载器去加载,然后引导(启动)类加载器则会去加载rt.jar中的java.lang.Object而不会去加载用户自己写的存在于classPath中的java.lang.Object类。同时双亲委派模型也避免了同一个类被重复加载的问题,保证了类的唯一性。

3.破坏双亲委派模型的介绍

看到这里大家可能有疑问了,既然双亲委派模型那么好用,为什么要去破坏它呢?举一个经典的场景,tomcat中的多个web项目如果依赖某个jar包的多个版本,而这个jar包中的某个类在不同版本中的具体内容也不相同,但是由于双亲委派模型的存在,相同的类只会加载一次,此时就会出现问题,所以此时就需要想办法去破坏双亲委派模型。

4.破坏双亲委派模型的例子

其实破坏双亲委派模型的例子并不少见,比较经典的例子有以下几种:

  • 双亲委派模型是JDK1.2之后引入的概念,而之前已经有类加载器相关概念了。所以当时用户写的自定义类加载器并不遵循双亲委派模型
  • 涉及SPI的加载的服务,如JNDI、JDBC等
  • 热替换、热部署等过于追求程序动态性的需求的出现
  • JDK9引入的模块化系统

(1) 需要加载SPI的服务破坏双亲委派模型的详解

以JDBC为例,我们都知道JDBC连接数据库第一步操作是加载驱动。以mysql-connector-java 5为例就是 Class.forName("com.mysql.jdbc.Driver") ,此时com.mysql.jdbc.Driver这个类被类加载器加载, 我们跟踪下源代码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
	
    // 当com.mysql.jdbc.Driver被加载的时候会执行静态代码块中的代码
    static {
        try {
            // 此处调用DriverManager的静态方法则DriverManager也会被类加载器加载
            // DriverManager是rt.jar包中的类,所以它是由Bootstrap ClassLoader加载的
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

 由上面com.mysql.jdbc.Driver源码可知,当加载驱动的时候会让类加载器去加载DriverManager这个类
 我们继续看一下DriverManager这个类的源码
 public class DriverManager {
 	// ...省略
    // 加载DriverManager的时候会执行这个静态代码块
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    // ...省略
    private static void loadInitialDrivers() {
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				// 此行代码会加载Driver的所有实现类
                // 如果按照双亲委派模型来看此时DriverManager是被Bootstrap ClassLoader加载的
                // 但是Driver的实现类却不是java中的基础类,不会被Bootstrap ClassLoader加载
                // 所以java设计团队就在此处破坏了双亲委派模型,具体做法我们继续跟踪源码
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
            }
        });
    }
}

ServiceLoader中的load方法源码:
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 这行代码是重点,用来获取线程上下文类加载器ThreadContext ClassLoader,
    // 这个类加载器就是Java设计团队为了解决调用SPI问题所设计的。
    // 这个类加载器通过java.lang.Thread中的setContextClassLoader()
    // 方法来设置。创建线程时如果没有设置这个类加载器,它将会从父线程中继承一个,
    // 如果全局范围都没有设置的话,它默认就是Application ClassLoader。
    // 所以有了这个类加载器就变相实现了Bootstrap ClassLoader可以加载调用SPI所需要的加载的类。
    // 对于JDBC而言就是Bootstrap ClassLoader通过ThreadContext ClassLoader
    // 来加载各种所需的Driver。
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

总结一下,Java设计团队为了解决涉及加载SPI问题设计出了特殊的类加载器:ThreadContext ClassLoader,利用该类加载器,然后需要加载SPI的时候就利用这个类加载器去加载所需的SPI类,实际上就是父类加载器利用子类加载器去加载类,这种行为模式实际上就是双亲委派模型的逆向。因此这种设计很明显破坏了双亲委派模型。

(2) Tomcat破坏双亲委派模型的详解

Tomcat需要打破双亲委派模型的原因:

  • Tomcat是一个web容器,它里面可以部署多个应用程序。那么这些应用程序有可能会依赖同一个类库的不同版本,因此要保证每一个应用程序都是独立的、互不影响则Tomcat需要去打破双亲委派模型。
  • Tomcat自身的类库也需要受保护,防止被随意修改和破坏,则需要使用特别的类加载器去加载自身的类库,而不是像双亲委派模型那样去委派父类加载器去加载。
  • JSP文件会被翻译成.class文件,如果使用了双亲委派模型,修改JSP文件后由于还是同一个class文件名则不会导致改文件被重新加载,所以Tomcat为了实现热部署,也需要去打破双亲委派模型。

Tomcat的类加载模型: image.png

Tomcat设计的类加载器的作用:

Common ClassLoader:Tomcat中的基础类加载器,加载的类可以被Tomcat和应用程序共享
Catalina ClassLoader: Tomcat私有的类加载器,加载的类对Tomcat可见,对应用程序不可见
Shared ClassLoader:应用程序之间的共享类加载器,加载的类对各应用程序可见,对Tomcat不可见
WebApp ClassLoader:当前应用程序私有的类加载器,对当前应用程序可见,对其他应用程序不可见
JasperLoader:主要负责加载JSP编译成的.class文件

Tomcat类加载模型的工作原理:

通过Common ClassLoader来加载共有类来供Tomcat和应用程序共同使用,Catalina ClassLoaderShared ClassLoader来加载Tomcat和应用程序各自需要的类并对对方保持不可见,从而实现Tomcat加载的类与应用程序加载的类之间相互隔离。每个应用程序都有独立的WebApp ClassLoader从而保证各个应用程序之间加载的类相互隔离互不影响。JasperLoader用来加载JSP编译成的.class文件,当jsp文件被修改后,该JasperLoader会被弃用,并重新生成一个JasperLoader从而实现热部署。