概述
前面通过类加载器系列(一)——类加载器的作用和分类介绍了类加载器的作用,本文主要讲述类加载器的双亲委派模型,并且通过源码的角度去深入理解它。
双亲委派模型
类加载器主要是为了把类加载到JVM中, 从JDK1.2开始,类加载采用双亲委派机制,那什么是双亲委派模型呢?
什么是双亲委派模型
一个类加载器在加载类的时候,它会委托父类加载器去加载,如果父类加载器还有父类加载器的话,继续委托,依次递归,如果父类加载器成功加载到类,则返回。如果父类加载器加载不到,则有当前类加载器尝试加载。
所以加载顺序为:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。
整个类加载流程的如下图:
一个类如果被加载过了,就不会被重复加载。
为什么采用双亲委派模型
前面讲明白了双亲委派机制,我们思考下为什么需要采用这种机制?其实这种机制有这么几个好处:
- 通过委派的方式,可以避免类的重复加载。当父加载器已经加载过某一个类时,子记载器就不会重新加载这个类。
- 通过双亲委派的方式,保证了安全性。比如我们的核心类库rt.jar是由启动类加载器
(BootstrapClassLoader)
加载的,采用双亲委派模型,可以避免有人自定义一个有破坏功能的java.lang.Integer被加载。这样可以有效的防止核心Java API被篡改。
源码解析类加载器
前面提到了父子加载器,但这里并不是java中的继承extends关系,而是一种组合关系。现在我们从代码层面去理解类加载器的双亲委派模型。
public static void main(String[] args) throws ClassNotFoundException { ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@18b4aac2 Class<?> clazz = classLoader.loadClass("java.lang.String"); System.out.println(clazz.getClassLoader()); //null }
上面是测试代码,加载类的核心方法是java.lang.ClassLoader.loadClass(String,boolean)
。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 检查类是否已经被加载过 Class<?> c = findLoadedClass(name); // 如果类没有被加载过 if (c == null) { long t0 = System.nanoTime(); try { // 当前类加载器是否有父类加载器 if (parent != null) { // 调用父加载器的loadClass()方法进行加载 c = parent.loadClass(name, false); } else { // 如果父类为空,则为启动类加载器进行加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 如果父类加载失败,抛出异常,不处理 // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 调用自己的findClass()方法进行加载 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; } }
以上是双亲委派机制的核心代码逻辑:
(1)先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
(2)判断当前加载器的父加载器是否为空,如果不为空,则调用parent.loadClass(name,false)接口进行加载。
(3)反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassorNull(name)接口,让启动类加载器进行加载。
(4)如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。该接口最终会调用java.lang.ClassLoader
接口的defineClass系列的native接口加载目标Java类。
ClassLoader源码
所有的类加载都是继承于ClassLoader这个类,它是通过组合的方式建立类加载器的父子关系。
关键成员变量
// 父类加载器引用 private final ClassLoader parent;
关键成员方法
public final ClassLoader getParent()
- 返回该类加载器的超类加载器
public Class<?> loadClass(String name) throws ClassNotFoundException
- 加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回 ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。
protected Class<?> findClass(String name) throws ClassNotFoundException
查找称为name的类, 返回java.lang.Class类的实例。 我们自定义的类加载器一般都是实现该方法,该方法会在检查完父类加载器之后被loadClass()方法调用。
protected final Class<?> defineClass(String name, byte[] b,int off,int len)
根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
- defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象。
- defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
protected final Class<?> findLoadedClass(String name)
链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
SecureClassLoader与URLClassLoader
SecureClassLoader
新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接打交道。URLClassLoader
提供了findClass()、findResource()等的实现,我们自定义类加载器可以直接继承该类,简单方便。
ExtClassLoader与AppClassLoader
AppClassLoader的父类加载器是ExtClassLoader,那么思考下这个设置父类加载器是在哪里进行的呢?
扩展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。
sun.misc.Launcher在应用启动的时候创建实例,实际情况我们不能够再该类的构造方法中打上断点,因为这个是系统类,再debug之前加载该类,我们看下该类的构造方法。
public Launcher() { Launcher.ExtClassLoader var1; try { // 创建扩展类加载器 var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { // 创建应用类加载器,同时这是它的父加载器为ExtClassLoader this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } // 同时设置当前线程的类加载器为AppClassLoader Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if (var2 != null) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { } catch (InstantiationException var6) { } catch (ClassNotFoundException var7) { } catch (ClassCastException var8) { } } else { var3 = new SecurityManager(); } if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
双亲委派模型是完美的吗?
双亲委派模型是完美的吗?答案显然是否定的。
检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。
试想一下,我们在java核心类库定义的规范或者接口,它是由启动类加载加载的,而这些核心接口的实现类是需要在下层应用层去实现的,如果完全按照双亲委派模型的话,就无法加载到这些扩展类,因为按照双亲委派模型规范,顶层的启动类加载器是无法加载到底层应用类。
那么有什么办法解决呢? 最典型的就是JDBC服务,它打破了原有的双亲委派模型,那是怎么做到的呢?
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "root");
上面是创建数据库连接的方式,DriverManager
会先被类加载器加载,因为java.sql.DriverManager类是位于rt.jar下面的 ,所以他会被启动类加载器加载。加载后会执行DriverManager的初始化,代码如下:
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { ..... ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); ..... }
DriverManager
中重要的方法是 ServiceLoader.load(Driver.class)
加载classpath下所有的驱动包。
但是问题来了,DriverManager
是有启动类加载加载的,根据双亲委派模型,他是无法加载到第三方类库中的类。那如何解决该问题呢?我们继续看代码...
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
发现没有,我们使用当前线程获取的类加载器去加载,那当前类加载默认是什么类加载器?答案就是应用类加载器AppClassLoader。我们在前面的源码分析中看到,Launcher的构造方法中设置了应用类加载器到当前线程中。
破坏双亲委派模型方法
上面介绍了通过Thread.currentThread().getContextClassLoader()
的方式来破坏双亲委派模型,那么还有其他什么方式或者案例吗?
- 通过线程上下文的方式破坏双亲委派模型,典型案例是JNDI、JDBC等SPI机制。
- 自定义类加载器ClassLoader, 比如tomcat中的
WebAppClassLoader
加载器。
我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。
Tomcat的类加载机制: 为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。
- 用户对程序动态性的追求而导致的。如:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等。比较典型的是OSGI模块化。在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构。
总结
本文讲清了双亲委派模型的机制,从源码角度分析了双亲委派模型的执行逻辑,辩证的看待双亲委派模型的有点和弊端。最后讲解了破坏双亲委派模型的方法,这里的“破坏”并不一定是带有贬义的,只要有明确的目的和充分的理由,突破旧有原则无疑是一种创新。
以上都是基于jdk8讲解的类加载器机制,实际上jdk9发生了一些变化,jdk9支持了模块化,去掉了扩展类加载器,但是为了兼容,依然保持这3层的结构,有兴趣的大家自己去看看。
后面我们学习如何自定义一个类加载器~