双亲委派模型源码分析
Launcher
分析类加载器源码要从 sun.misc.Launcher.class 文件看起, 关键代码已添加注释,同时可以在此类中看到 ExtClassLoader 和 AppClassLoader 的定义,也验证了我们上文提到的他们不是继承关系,而是通过指定 parent 属性来形成的组合模型
进入上面第25行的 loadClass 方法中
我们看到方法有同步块(synchronized), 这也就解释了文章开头第2个问题,多线程情况不会出现重复加载的情况。同时会询问parent classloader是否有加载,如果没有,自己尝试加载。
URLClassLoader中的 findClass方法:
借用网友的一个加载时序图来解释整个过程更加清晰:
双亲委派模型的破坏
Java本身有一套资源管理服务JNDI,是放置在rt.jar中,由启动类加载器加载的。以对数据库管理JDBC为例,java给数据库操作提供了一个Driver接口:
然后提供了一个DriverManager来管理这些Driver的具体实现:
这里省略了大部分代码,可以看到我们使用数据库驱动前必须先要在DriverManager中使用registerDriver()注册,然后我们才能正常使用。
不破坏双亲委派模型的情况(不使用JNDI服务)
我们看下mysql的驱动是如何被加载的:
核心就是这句Class.forName()触发了mysql驱动的加载,我们看下mysql对Driver接口的实现:
可以看到,Class.forName()其实触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。这个时候,我们通过DriverManager去获取connection的时候只要遍历当前所有Driver实现,然后选择一个建立连接就可以了。
破坏双亲委派模型的情况
在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个,然后使用的时候就直接这样就可以了:
可以看到这里直接获取连接,省去了上面的Class.forName()注册过程。
现在,我们分析下看使用了这种spi服务的模式原本的过程是怎样的:
- 第一,从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”
- 第二,加载这个类,这里肯定只能用class.forName("com.mysql.jdbc.Driver")来加载
好了,问题来了,Class.forName()加载用的是调用者的Classloader,这个调用者
DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver肯定不在<JAVA_HOME>/lib下,所以肯定是无法加载mysql中的这个类的。这就是双亲委派模型的局限性了,父级加载器无法加载子级类加载器路径中的类。
那么,这个问题如何解决呢?按照目前情况来分析,这个mysql的drvier只有应用类加载器能加载,那么我们只要在启动类加载器中有方法获取应用程序类加载器,然后通过它去加载就可以了。这就是所谓的线程上下文加载器。
文章前半段提到线程上下文类加载器可以通过 Thread.setContextClassLoaser() 方法设置,如果不特殊设置会从父类继承,一般默认使用的是应用程序类加载器
很明显,线程上下文类加载器让父级类加载器能通过调用子级类加载器来加载类,这打破了双亲委派模型的原则
现在我们看下DriverManager是如何使用线程上下文类加载器去加载第三方jar包中的Driver类的,先来看源码:
使用时,我们直接调用DriverManager.getConnection() 方法自然会触发静态代码块的执行,开始加载驱动然后我们看下ServiceLoader.load()的具体实现:
继续向下看构造函数实例化 ServiceLoader 做了哪些事情:
查看 reload() 函数:
继续查看LazyIterator构造器,该类同样实现了Iterator接口:
实例化到这里我们也将上下文得到的类加载器实例化到这里,来回看ServiceLoader 重写的 iterator() 方法:
上面next() 方法调用了lookupIterator.next(),这个lookupIterator 就是刚刚实例化的 LazyIterator(); 来看next方法
继续查看nextService 方法:
终于到这里了,在上面 nextService函数中第8行调用了c = Class.forName(cn, false, loader) 方法,我们成功的做到了通过线程上下文类加载器拿到了应用程序类加载器(或者自定义的然后塞到线程上下文中的),同时我们也查找到了厂商在子级的jar包中注册的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了同时在第16行完成Driver的实例化,等同于new Driver(); 文章开头的问题在理解到这里也迎刃而解了
JAVA热部署实现
首先谈一下何为热部署(hotswap),热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。Java 类是通过 Java 虚拟机加载的,某个类的 class 文件在被 classloader 加载后,会生成对应的 Class 对象,之后就可以创建该类的实例。默认的虚拟机行为只会在启动时加载类,如果后期有一个类需要更新的话,单纯替换编译的 class 文件,Java 虚拟机是不会更新正在运行的 class。如果要实现热部署,最根本的方式是修改虚拟机的源代码,改变 classloader 的加载行为,使虚拟机能监听 class 文件的更新,重新加载 class 文件,这样的行为破坏性很大,为后续的 JVM 升级埋下了一个大坑。
另一种友好的方法是创建自己的 classloader 来加载需要监听的 class,这样就能控制类加载的时机,从而实现热部署。
热部署步骤:
- 销毁自定义classloader(被该加载器加载的class也会自动卸载);
- 更新class
- 使用新的ClassLoader去加载class
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。
- 加载该类的ClassLoader已经被GC。
- 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法
自定义类加载器
要创建用户自己的类加载器,只需要继承java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,即指明如何获取类的字节码流。
如果要符合双亲委派规范,则重写findClass方法(用户自定义类加载逻辑);要破坏的话,重写loadClass方法(双亲委派的具体逻辑实现)。