1. java类加载器
近来了解tomcat的类加载机制,所以先回顾一下java虚拟机类加载器,如果从java虚拟机的角度来看的话,其实类加载器只分为两种:一种是启动类加载器(即Bootstrap ClassLoader),通过使用JNI来实现,我们无法获取到到它的实例;另一种则是java语言实现java.lang.ClassLoader
的子类。一般从我们的角度来看,会根据类加载路径会把类加载器分为3种:Bootstrap ClassLoader,ExtClassLoader,AppClassLoader.后两者是sun.misc.Launcher
类的内部类,而前者在JDK源码中是没有与之对应的类的,倒是在sun.misc.Launcher
中可以看到一些它的加载路径信息。如果找不到sun的源码,可以下载OpenJDK的来看一下。
Bootstrap ClassLoader: 引导类加载器,从%JAVA_RUNTIME_JRE%/lib目录加载,但并不是将该目录所有的类库都加载,它会加载一些符合文件名称的,例如:rt.jar,resources.jar等。在sun.misc.Launcher
源码中也可以看得它的加载路径:
private static String bootClassPath = System.getProperty("sun.boot.class.path");
或者配置-Xbootclasspath参数指定加载的路径,通过获取环境变量sun.boot.class.path
看一下到底具体加载了那些类:
D:\Program Files\Java\jdk1.7.0_67\jre\lib\resources.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\rt.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\sunrsasign.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jsse.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jce.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\charsets.jar
D:\Program Files\Java\jdk1.7.0_67\jre\lib\jfr.jar
D:\Program Files\Java\jdk1.7.0_67\jre\classes
Extension ClassLoader:扩展类加载器,实现类为sun.misc.Launcher$ExtClassLoader
,加载%JAVA_RUNTIME_JRE%/lib/ext/目录下的jar包,也可以在sun.misc.Launcher
源码中也可以看得它的加载路径:
String s = System.getProperty("java.ext.dirs");
通过获取java.ext.dirs
环境变量打印一下:
D:\Program Files\Java\jdk1.7.0_67\jre\lib\ext
Appication ClassLoader:应用程序类加载器,或者叫系统类加载器,实现类为sun.misc.Launcher$AppClassLoader
。从sun.misc.Launcher
的构造函数中可以看到,当AppClassLoader
被初始化以后,它会被设置为当前线程的上下文类加载器以及保存到Launcher
类的loader属性中,而通过ClassLoader.getSystemClassLoader()
获取的也正是该类加载器(Launcher.loader)。应用类加载器从用户类路径中加载类库,可以在源码中看到:
final String s = System.getProperty("java.class.path");
1.1 类关系
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yOgvgKNl-1658975908511)(http://note.youdao.com/yws/api/personal/file/D20FB41BFE794E0196739C105761133C?method=download&shareKey=cd60915e9cd05fdbcd629fd0ef204725)]
由图看到Bootstrap ClassLoader并不在继承链上,因为它是java虚拟机内置的类加载器,对外不可见。可以看到顶层ClassLoader
有一个parent属性,用来表示着类加载器之间的层次关系(双亲委派模型);注意,ExtClassLoader
类在初始化时显式指定了parent为null,所以它的父类加载器默认为Bootstrap ClassLoader
。在tomcat中都是通过扩展URLClassLoader
来实现自己的类加载器。
1.2 双亲委托模型
这3种类加载器之间存在着父子关系(区别于java里的继承),子加载器保存着父加载器的引用。当一个类加载器需要加载一个目标类时,会先委托父加载器去加载,然后父加载器会在自己的加载路径中搜索目标类,父加载器在自己的加载范围中找不到时,才会交还给子加载器加载目标类。
采用双亲委托模式可以避免类加载混乱,而且还将类分层次了,例如java中lang包下的类在jvm启动时就被启动类加载器加载了,而用户一些代码类则由应用程序类加载器(AppClassLoader)加载,基于双亲委托模式,就算用户定义了与lang包中一样的类,最终还是由应用程序类加载器委托给启动类加载器去加载,这个时候启动类加载器发现已经加载过了lang包下的类了,所以两者都不会再重新加载。当然,如果使用者通过自定义的类加载器可以强行打破这种双亲委托模型,但也不会成功的,java安全管理器抛出将会抛出java.lang.SecurityException
异常。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpPgUo6q-1658975908513)(http://note.youdao.com/yws/api/personal/file/A75B230FED5F4FA7BBC9BF82FBB4F52F?method=download&shareKey=e4f9e61797b14f87e7b067c086dd8f78)]
- 启动类加载器是扩展类加载器的父类加载器:扩展类加载器在
sun.misc.Launcher
构造函数中被初始化,它的父类加载器被设置了为null,那为什么还说启动类加载器是它的父加载器?看一下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) { // 父类加载器不为null,则调用父类加载器尝试加载
c = parent.loadClass(name, false);
} else { // 父类加载器为null,则调用本地方法,交由启动类加载器加载,所以说ExtClassLoader的父类加载器为Bootstrap ClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) { //仍然加载不到,只能由本加载器通过findClass去加载
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;
}
}
从代码中看到,如果parent==null,将会由启动类加载器尝试加载,所以扩展类加载器的父类加载器是启动类加载器。
- 扩展类加载器是应用程序类加载器的父类加载器:这个比较好理解,依然是在
sun.misc.Launcher
构造函数初始化应用程序类加载器时,指定了ExtClassLoader为AppClassLoader的父类加载器:
loader = AppClassLoader.getAppClassLoader(extcl);//loader是ClassLoader的属性,extcl是扩展类加载器实例
- 应用程序类加载器是自定义类加载器的父类加载器:这里指的是使用默认构造函数进行自定义类加载器(否则 你可以指定parent来构造一个父加载器为ExtClassLoader的自定义类加载器),无论是通过扩展ClassLoader还是URLClassLoader最终都会获取系统类加载器(AppClassLoader)作为父类加载器:
protected ClassLoader() {
//调用getSystemClassLoader方法获取系统类加载器作为父类加载器
this(checkCreateClassLoader(), getSystemClassLoader());
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader(); //初始化系统类加载器
.....
return scl;
}
private static synchronized void initSystemClassLoader() {
......
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
......
scl = l.getClassLoader(); //这里拿到的就是在Launcher构造函数中构造的AppClassLoader实例
......
}
}
2. tomcat7类加载器
tomcat作为一个java web容器,也有自己的类加载机制,通过自定义的类加载机制以实现共享类库的抽取,不同web应用之间的资源隔离还有热加载等功能。除了一些java自身的一些类加载器处,它实现的主要类加载器有:Common ClassLoader,Catalina ClassLoader,Shared ClassLoader以及WebApp ClassLoader.通过下面类关系图以及逻辑关系图,同时对比上文内容梳理这些类加载器之间的关系。
2.1 类关系图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2iNbNarv-1658975908513)(http://note.youdao.com/yws/api/personal/file/0E42DAE4DE4341118C33D79AD5B0AB7F?method=download&shareKey=5d9d389c462ece01488325496fea1ee7)]
从图中看到了Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,只是它们的类加载路径不一样,在tomcat/conf/catalina.properties配置文件中配置(common.loader,server.loader,shared.loader).WebAppClassLoader继承自WebAppClassLoaderBase,基本所有逻辑都在WebAppClassLoaderBase为中实现了,可以看出tomcat的所有类加载器都是以URLClassLoader为基础进行扩展。
2.2 逻辑关系图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gvHRChhE-1658975908514)(http://note.youdao.com/yws/api/personal/file/4245B0B09C1F4DF9A544A17D07C2D817?method=download&shareKey=a1155eca3e460782c32f511ca0986aa8)]
上面说到Common,Catalina,Shared类加载器是URLClassLoader类的一个实例,在默认的配置中,它们其实都是同一个对象,即commonLoader,结合初始化时的代码(只保留关键代码):
private void initClassLoaders() {
commonLoader = createClassLoader("common", null); // commonLoader的加载路径为common.loader
if( commonLoader == null ) {
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader); // 加载路径为server.loader,默认为空,父类加载器为commonLoader
sharedLoader = createClassLoader("shared", commonLoader); // 加载路径为shared.loader,默认为空,父类加载器为commonLoader
}
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception {
String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent; // catalinaLoader与sharedLoader的加载路径均为空,所以直接返回commonLoader对象,默认3者为同一个对象
}
在上面的代码初始化时很明确是指出了,catalina与shared类加载器的父类加载器为common类加载器,而初始化commonClassLoader时父类加载器设置为null,最终会调到createClassLoader
静态方法:
public static ClassLoader createClassLoader(List<Repository> repositories,
final ClassLoader parent)
throws Exception {
.....
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array); //该构造方法默认获取系统类加载器为父类加载器,即AppClassLoader
else
return new URLClassLoader(array, parent);
}
});
}
在createClassLoader
中指定参数parent==null
时,最终会以系统类加载器(AppClassLoader)作为父类加载器,这解释了为什么commonClassLoader的父类加载器是AppClassLoader.
一个web应用对应着一个StandardContext
实例,每个web应用都拥有独立web应用类加载器(WebClassLoader),这个类加载器在StandardContext.startInternal()
中被构造了出来:
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
这里getParentClassLoader()
会获取父容器StandarHost.parentClassLoader
对象属性,而这个对象属性是在Catalina$SetParentClassLoaderRule.begin()
初始化,初始化的值其实就是Catalina.parentClassLoader
对象属性,再来跟踪一下Catalina.parentClassLoader
,在Bootstrap.init()
时通过反射调用了Catalina.setParentClassLoader()
,将Bootstrap.sharedLoader
属性设置为Catalina.parentClassLoader
,所以WebClassLoader的父类加载器是Shared ClassLoader.
2.3 类加载逻辑
tomcat的类加载机制是违反了双亲委托原则的,对于一些未加载的非基础类(Object,String等),各个web应用自己的类加载器(WebAppClassLoader)会优先加载,加载不到时再交给commonClassLoader走双亲委托。具体的加载逻辑位于WebAppClassLoaderBase.loadClass()
方法中,代码篇幅长,这里以文字描述加载一个类过程:
- 先在本地缓存中查找是否已经加载过该类(对于一些已经加载了的类,会被缓存在
resourceEntries
这个数据结构中),如果已经加载即返回,否则 继续下一步。 - 让系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
- 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
- 最后还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
第3第4两个步骤的顺序已经违反了双亲委托机制,除了tomcat之外,JDBC,JNDI,Thread.currentThread().setContextClassLoader();
等很多地方都一样是违反了双亲委托。
3. 参考
《深入理解java虚拟机》
http://blog.csdn.net/zhoudaxia/article/details/35824249
http://blog.csdn.net/kkdelta/article/details/7758916