一)类加载
//这里是从think in jvm第三部分第七章里面引用的加了一些自己的话,回顾一下类加载,为理解双亲委派破坏做一下复习
在java当中要使用一个类,需要事先被classloader加载,触发类初始化clinit的动作有
1)new getstatic putstatic invokestatic,四条指令分别对应了new对象,读取、设置静态变量,调用静态方法
总结起来就是使用或创建一个类的时候,创建包括newInstance,这里我划分为A类型
2)反射,使用的方法来获取类对象,其实在forName这里就有了加载动作,后期使用时的初始化和上面的创建是一样,和1)可以划分为一类还是A类型
3)初始化一个类先初始化其父类,这里用继承的思想不难理解,a extends b,a就是一个b,理解起来不难,要看代码估计要去看jvm源码了,这里分为B类型
4)虚拟机启动调用主类的main方法,还是A类型
5)jdk1.7中使用MethodHandle,lookup.findStatic后调用方法会初始化(参考MethodHandle注释里面的示例),暂时理解未A类
总体看下来就是三种情况:创建,调用静态方法,子类初始化
类加载器有基本的层级关系
System.out.println(System.getProperty("sun.boot.class.path"));//boot,jre的包
java.ext.dirs//ext,扩展
java.class.path//AppClassLoader,当前应用的classes和jar包
系统执行了boot后才会执行ext再执行app,执行的按双亲委派搜索,父加载器加载过的就不会再加载了
可以看DriverManager类进行进一步了解了,由这里可以引出spi的学习,暂不拉开
二)tomcat
整理了一下关于tomcat的类加载
从6.x开始的tomcat版本,已经没有了common/share/server的包存放路径
为了将同一个tomcat webapp下的不同应用隔离开,所以需要不同的类加载器,每一个应用对应到的tomcat组件就是context
在StandardHost的事件监听器hostConfig中的deployDirectories(或者其他两种解析方式)方法开启了一个单线程的线程池,用于多个应用发布不同的context
**1.webappLoader **
针对每个应用有一个webappLoader加载器,其classloader父类继承前面的加载器,所以查看代码
org.apache.catalina.core.StandardContext.startInternal()
if (getLoader() == null) {
//点进去会看到,在初次启动时,返回host的加载器
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
//这个地方将新建的webappLoader传入,在restart情况下会替换原有的loader,同时将原有的loader停掉
setLoader(webappLoader);
}
2.bindThread()方法
再往下看会看到这么一段有趣的代码,刚好可以理解一下破坏双亲体系的一种方式
ClassLoader oldCCL = bindThread();
...loader.start();//启动刚刚建好的webappLoader
unbindThread(oldCCL);
oldCCL = bindThread();
...others.start()
unbindThread(oldCCL);
这个方法调用了两次,看bindThread注释:Bind current thread, both for CL purposes and for JNDI ENC support
方法将旧线程记录下来返回,并且将当前线程的上下文加载器设置为webappLoader的类加载器
再看unbindThread是将记录的加载器设置回去
所以说多个应用下的ContextClassLoader在最终都是同一个
protected ClassLoader bindThread() {
ClassLoader oldContextClassLoader =
Thread.currentThread().getContextClassLoader();
if (getResources() == null)
return oldContextClassLoader;
if (getLoader() != null && getLoader().getClassLoader() != null) {
//首次调用时getLoader().getClassLoader()应该是空的,并未运行本段代码,在后续的loader.start()中创建了新的classloader,再进来这段方法把刚才创建的classloader
Thread.currentThread().setContextClassLoader
(getLoader().getClassLoader());
}
DirContextURLStreamHandler.bindThread(getResources());
if (isUseNaming()) {
try {
ContextBindings.bindThread(this, this);
} catch (NamingException e) {
// Silent catch, as this is a normal case during the early
// startup stages
}
}
return oldContextClassLoader;
}
protected void unbindThread(ClassLoader oldContextClassLoader) {
if (isUseNaming()) {
ContextBindings.unbindThread(this, this);
}
DirContextURLStreamHandler.unbindThread();
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
}
3.bind了什么
先从Thread.currentThread().getContextClassLoader()开始看起
@CallerSensitive
public ClassLoader getContextClassLoader() {
if (contextClassLoader == null)
return null;
...permission check code
return contextClassLoader;
}
方法返回的是当前线程的contextClassLoader,然后我们寻找其初始化的代码
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
Thread parent = currentThread();
...
this.contextClassLoader = parent.getContextClassLoader();
}
顺带还看到了线程之间的关系,线程创建时的线程作为被创建线程的parent,同时用了同一个contextClassLoader,如果没有调用setContextClassLoader,那么我们整个代码过程中的contextClassLoader将会是主线程的类加载器,也就是AppClassLoader
回想到tomcat启动的时候,在Bootstrap类中,有一个init方法
public void init() throws Exception {
// Set Catalina path
setCatalinaHome();
setCatalinaBase();
initClassLoaders();
Thread.currentThread().setContextClassLoader(catalinaLoader);
...
}
在方法initClassLoaders中看到catalinaLoader = createClassLoader("server", commonLoader);
createClassLoader中获取了server.loader这个系统参数的值(这个值是catalina包和启动脚本传的值),解析出来是tomcat的lib目录,将lib目录添加进去,然后创建了一个URLClassLoader,这个loader可以解析jar包,且父加载器为appClassloader
了解了ContextClassLoader后回到StandardContext处,webappLoader构造方法追踪后发现其parentClassLoader是系统的appClassloader,且其classLoader属性为一个WebappClassLoaderBase,webappLoader.classLoader解析加载的资源包括当前应用的classes,WEB-INF/lib目录,以及一个MANIFEST.MF文件
到这里基本上就明白了,每一个类加载器都有自己加载的范围,我们的tomcat由于需要加载各个目录下的jar和classes,而路径是启动前不知道的,所以引入了contextClassLoader来动态加载需要的资源,且context的父加载器为系统最开始的appClassloader能够获取公用的java类,不影响整体,由此将不同的应用隔离开来,brilliant!
三)正题
为什么会说这里违反了双亲委派,WebappClassLoaderBase#loadClass重写了ClassLoader的loadClass方法
查询类是否加载并没有直接交给父类,而是按照
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
// (0.1) Check our previously loaded class cache
clazz = findLoadedClass(name);
前面两个是查看是否已经加载过
clazz = j2seClassLoader.loadClass(name);//这个是用extloader检查一些通用类,防止加载J2SE classes(加载类是通过全路径判断的,防止应用中重新定义一些通用类,如String这种)
boolean delegateLoad = delegate || filter(name);//filter(name)判断类的包路径是否要交给父类加载
clazz = Class.forName(name, false, parent);//如果判断delegateLoad=true就由父类加载,通过debug几个项目发现delegateLoad=false始终是false
//****findClass****自己加载类,假如自己的资源中没有这个类,才会到下一步去让父类加载,说了半天就这两行代码
clazz = findClass(name);
clazz = Class.forName(name, false, parent);
bind和unbind之间代码应该都是针对非应用下的操作,防止有类被覆盖了
还有一点,类加载过程中会根据带\的全路径到资源列表中查找类.class文件,假如这个类在jar包中,会调用获取对应的文件(jar包在系统中实际是一个一个的Url),如果资源不在自己这里就会找不到
举个栗子
在eclipse的开发过程中,使用到tomcat的servlet.jar包的时候,pom中会带上这个包,在发布时假如不将这个包除去,在tomcat启动时会加载一次servlet包,随后在应用处会再尝试加载一次,就有一个冲突的错误出现
附上一个图