最近在读许令波的深入分析Java Web技术内幕
一书,对于学习Java以来一直有的几个疑惑得到了解答,遂记录下来.
ClassLoader
顾名思义是类加载器(准确来说为JVM平台类加载器抽象父类),主要功能负责将Class加载到JVM中,其所使用的加载策略叫做双亲委派模型.其主要有如下方法
JVM平台提供三个ClassLoader:
那么不算Bootstrap ClassLoader
,JVM体系的ClassLoader结构如下
ClassLoader作为抽象父类其实是一装饰者模式中的Decorator角色,AppClassLoader本质上是对ExtClassLoader的增强处理,再看初始化方式可以简化为
Launcher.AppClassLoader.getAppClassLoader(Launcher.ExtClassLoader.getExtClassLoader())
是不是和IO套用初始化很像?其实双亲委派按照我的理解本质上就是装饰者模式的应用,使用组合代替了继承只不过这个被装饰者叫做parent,思想上一致只是用法的不同.
另外这张图也说明如果想要实现自己的ClassLoader,只需要继承java.net.URLClassLoader
,然后自定义加载逻辑即可.
查找Class字节流,然后根据这个创建Java类的过程,这里要注意对于数组来说并没有对应的字节流,是由JVM直接生成,因此加载只适用于数组以外的Class文件流。
接下来重点看loadClass()
方法,该方法为加载class二进制文件的核心方法.
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 {
//当父加载器不存在的时候会尝试使用BootStrapClassLoader作为父类
if (parent != null) {
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
}
//c为null则证明父加载器没有加载到,进而使用子类本身的加载策略`findClass()`方法
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
当父类不存在时调用java.lang.ClassLoader#findBootstrapClass
使用BootStrapClass加载,该方法是一个native方法,也进一步说明了Bootstrap Classloader
与JVM体系的ClassLoader没什么父子类关系.完全独立.
链接的目的是把上述加载的类合并到JVM当中,使之可以运行,链接过程又分为验证 -> 准备 -> 解析
三个流程.
验证
主要是确保加载的流符合JVM所定义的规范.
准备
阶段主要是为静态字段分配内存,以及一些额外的预处理,
解析
阶段主要是把编译期生成的符号引用变成转换为实际引用,所谓的符号引用是由于编译期无法确定其他类是否被加载,因此当该类使用外部类的字段或者方法时,是预先生成符号引用存放在Class文件的常量池中,如果对应符号引用的类还未被加载,那么会触发其加载流程,但不一定会触发其解析流程。
类加载过程的初始化主要针对静态字段,因为一个类只能被初始化一次,因此也确保了静态字段只被初始化一次,具体流程如下:
<cinit>
方法中,然后执行该方法初始化静态字段。这是类的初始化流程。另外JVM规定以下情况会触发类的初始化:
那么开始回答问题
1. 双亲委派模型是什么?
上述加载流程是 使用parent加载器加载类
-> parent不存在使用BootStrapClassLoader加载
-> 加载不到则使用子类的加载策略
,这里要注意BootStrapClassLoader
是由C++实现的JVM内部的加载工具,其没有对应的Java对象,因此不在这个委派体系中,类加载器本质上是装饰者模式组合思想的应用.
那么双亲是什么? 看ClassLoader的注释就能发现这只是个翻译问题parent->双亲,明明是单亲委派,装饰者模式是单类增强委托. RednaxelaFX关于这点的证实
2. 委派模型如何实现?
委派模型从设计模式角度来看是一种组合设计,双亲委派这里更像是使用桥接模式实现的委托机制,由继承图可以发现ExtClassloader
与AppClassloader
处于同一层级,其内部又可以通过持有对应的private final ClassLoader parent
达到桥接委派的目的。
3. 为什么使用委派模型? 回答这个问题要先了解Java中是如何判定两个类是同一个类状况,如下段官方所说,也就是全类名(包括包名)相同并且他们的类加载器相同,那么两个对象才是等价的.
At run time, several reference types with the same binary name may be loaded simultaneously by different class loaders.
These types may or may not represent the same type declaration.
Even if two such types do represent the same type declaration, they are considered distinct.
对于Object类因为父加载器先加载所以能保证对于所有Object的子类其所对应的Object都是由同一个ClassLoader所加载,也就保证了对象相等. 简单来说委托类优先模式保证了加载器的优先级问题,让优先级高的ClassLoader先加载,然后轮到优先级低的.
热部署对于开发阶段的实用性极高,利用Jrebel等工具可以极大的节省应用调试时间.关于热加载技术可以参考文章http://www.hollischuang.com/archives/606,
对于一个被ClassLoader加载到内存的类来说,再次加载的时候就会被findLoadedClass()
方法所拦截,其判断该类已加载,则不会再次加载,那么热加载的技术本质是要替换到已加载的类.
对于Spring Boot devtools的restart技术,其是使用了两个ClassLoader,对于开发者所写的类使用自定义的ClassLoader,对于第三方包则使用默认加载器,那么每当代码有改动需要热加载时,丢弃自定义的ClassLoader所加载的类,然后重新使用其加载,如此做到了热部署.
对于Jrebel使用的貌似是修改类的字节码方式,具体不是很懂也就不讨论了.
对于Tomcat,其热部署技术是每次清理之前的引用,然后创建一个新的ClassLoaderWebClassLoader
来重新加载应用,这个加载使得永久代中对象增多,那么清理要求是full GC,这个是不可控的,所以也就导致了Tomcat热部署频繁会触发java.lang.OutOfMemoryErrorPermGen space
这个bug.
ClassLoader的委派模型使得很容易扩展自定义的类加载器,那么基本步骤 定义自己的类加载器
-> 加载指定jar
-> 创建所需要的应用实例
,大概代码如下.
String jarPath = "/Users/niuli/workspace/quding-git/quding-study/helloworld/target/hello-world-1.0-SNAPSHOT.jar";
URL jarUrl = new File(jarPath).toURI().toURL();
//加载该jar
URLClassLoader loader = new URLClassLoader(new URL[]{jarUrl},Thread.currentThread().getContextClassLoader());
//获取插件Class对象
Class helloClass = loader.loadClass("com.itoolshub.hello.HelloWorld");
//创建该对象
IHelloWorldService helloWorldService = (IHelloWorldService) helloClass.newInstance();
//调用方法
helloWorldService.sayHello();
另外插件模式的话一般还会有一些配置文件plugin.xml,告诉系统主要对外提供服务的类是什么以及一些默认配置等.不过大概思路都是大同小异.
另外既然有装载也就有卸载,卸载的必要条件是以下三个外,另外类是装载在永久代,那么卸载的触发也就是full GC才会去清理永久代中没有被强引用指向的类.
双亲委派模型中,从顶层到底层,都是哪些类加载器,分别加载哪些类? Bootstrap ClassLoader: JVM自身需要的一些类,当Classloader中parent为null时执行该加载器尝试加载 ExtClassLoader: 主要加载java.ext.dirs AppClassLoader; 主要加载java.class.path下的类,包括用户定义的类 纠正双亲委派模型,实际上就是装饰者模式应用.
双亲委派模型破坏举例 1.双亲委派模型是JDK1.2发布的模式,在这之前开发者是重写loadClass()这个方法实现自定义加载逻辑,该方法中又是双亲委派模型的关键算法,那么重写完全可以破坏该模型. 2.Java的SPI机制
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
SPI一些官方接口是定义在rt.jar中的,那么其所使用的是BootstrapClassloader,然而我们引入的第三方实现却是AppClassloader所管理,那么这里的问题就是BootstrapClassloader无法加载AppClassloader所管理的内容,也就是双亲委派无法逆序执行.那么想要逆序就需要破坏这一约束.利用Thread中上下文加载器来实现,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是AppClassloader
,那么在BootstrapClassloader
使用SPI加载时,则会利用线程上下文加载器委托AppClassloader
加载其实现类,那么这一过程与双亲委派相反,是破坏双亲委派原则的一种做法.
try {
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);
3.热部署,热加载技术等都是破坏了双亲委派模型.