这是我参与更文挑战的第3天,活动详情查看:更文挑战
背景简介
在之前调研canal adapter的两个类加载器导致的启动出错的问题时,顺便把Tomcat的类加载机制也整理了一下,下面是自己梳理的笔记,并留了一个未解决的问题,希望高手来回答一下
类加载器
如何判断两个类是否相等
对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性,每一个类,都拥有一个独立的类名称空间。也就是说:比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
默认情况下,用户自定义的类使用 AppClassLoader 加载,AppClassLoader 的父加载器为 ExtClassLoader,但是 ExtClassLoader 的父加载器却显示为空,因为启动类加载器属于 JVM 的一部分,它不是由 Java 语言实现的,在 Java 中无法直接引用,所以才返回空。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
如果没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户完全可以恶意编写一个java.lang.Object类,然后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
WEB服务器的类加载器
主流的JavaWeb服务器,如Tomcat、Jetty、WebLogic、WebSphere等都实现了自己定义的类加载器,因为一个功能健全的Web服务器,都要解决如下的这些问题:
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。
- 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。
- 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。
由于存在上述问题,在部署Web应用时,单独的一个ClassPath就不能满足需求了,所以各种Web服务器都提供了好几个有着不同含义的ClassPath路径供用户存放第三方类库,这些路径一般会以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务对象,通常每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。
Tomcat的类加载器
四个类库目录
在Tomcat目录结构中,可以设置3组目录(/common/*、/server/和/shared/,但默认不一定是开放的,可能只有/lib/目录存在)用于存放Java类库,另外还应该加上Web应用程序自身的“/WEB-INF/”目录,一共4组。
把Java类库放置在这4组目录中,每一组都有独立的含义:
- 放置在/common目录中。类库可被Tomcat和所有的Web应用程序共同使用
- 放置在/server目录中。类库可被Tomcat使用,对所有的Web应用程序都不可见
- 放置在/shared目录中。类库可被所有的Web应用程序共同使用,但对Tomcat应用本身不可见
- 放置在/WebApp/WEB-INF目录中。类库仅仅可以被该Web应用程序使用,对Tomcat和其他Web应用程序都不可见
Tomcat6之后的变化
在Tomcat6及之后的版本简化了默认的目录结构,只有指定了tomcat/conf/catalina.properties配置文件的server.loader和share.loader项后才会真正建立Catalina类加载器和Shared类加载器的实例,否则会用到这两个类加载器的地方都会用Common类加载器的实例代替,而默认的配置文件中并没有设置这两个loader项,所以Tomcat6之后也顺理成章地把/common、/server和/shared这3个目录默认合并到一起变成1个/lib目录,这个目录里的类库相当于以前/common目录中类库的作用,是Tomcat的开发团队为了简化大多数的部署场景所做的一项易用性改进。
如果默认设置不能满足需要,用户可以通过修改配置文件指定server.loader和share.loader的方式重新启用原来完整的加载器架构。
Bootstrap ClassLoader
启动类加载器,它负责将 <Java_Runtime_Home>/lib下面的类库加载到内存中,但并不是默认加载所有的类库,它会加载一些符合文件名称的,例如:rt.jar,resources.jar等
由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
Extension ClassLoader
扩展类加载器,实现类为sun.misc.Launcher$ExtClassLoader,它负责将< Java_Runtime_Home >/lib/ext或者由系统变量 java.ext.dir指定位置中的类库加载到内存中。
开发者可以直接使用标准扩展类加载器。
Appication ClassLoader
应用程序类加载器,或者叫系统类加载器,实现类为sun.misc.Launcher$AppClassLoader。
它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。
从sun.misc.Launcher的构造函数中可以看到,当AppClassLoader被初始化以后,它会被设置为当前线程的上下文类加载器以及保存到Launcher类的loader属性中,而通过ClassLoader.getSystemClassLoader()获取的也正是该类加载器(Launcher.loader)。
Tomcat加载类的核心代码
精简后的核心代码如下:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
// 1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 尝试用系统类加载器类加载
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索class并加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}
复制代码
核心流程如下:
- 先在本地缓存中查找是否已经加载过该类(本地加载的类会被缓存在resourceEntries这个数据结构中),这一步适用于自己写的class类.
- 查看系统类加载器的缓存中是否加载过这个类,这一步适用于系统标准类.
- 如果所有缓存中都没有,说明没加载过,则先使用系统类加载器(AppClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖。为了防止覆盖基础类实现,这里会判断class是不是JVMSE中的基础类库中类。
- 判断delegateLoad开关是否打开,如果打开,会完全按照JVM的”双亲委托”机制流程加载类
- 默认是没打开的,会优先使用WebappClassLoader自己处理加载类,按照WEB-INF/classes、WEB-INF/lib的加载顺序,如果还是加载不到的话,则委托父类加载器(Common ClassLoader)去加载。
未解决的一个疑问
问题描述
查了网上很多资料,仍然不是很确定getJavaseClassLoader()这个方法拿到的是哪个系统类加载器,下面是一些资料摘抄:
- 有的说是拿到的Ext Class Loader
- 有的说是拿到的sun.misc.Launcher$AppClassLoader
- 有的说是拿到的BootStrap Class Loader
- 有的说法是这个类加载器大多数情况下是bootstrap
- 有的说法是因为注释写的是系统类加载器,而AppClassLoader又叫系统类加载器,所以应该是AppClassLoader
下面是从一个源码中看到的注释,大概意思它指向一个内置的class loader,指向哪个不确定,有可能是三个内置class loader中的一个
// The getJavaseClassLoader method returns a reference to javaseClassLoader, which is a reference variable
// References are java built-in loads: bootstrapClassLoader, or systemClassLoader, or extClassLoader.
// This starts with a loader built into java to prevent user-defined classes in applications and overwriting core java classes.
// Instead of loading with the built-in loader first, load the classes in the local library first, if the application class and the java core class
// With the same name, the java core class cannot be loaded.
复制代码
自己的理解
结合大部分资料,自己觉得应该是AppClassLoader,然后它仍然会按照双亲委派机制,会逐个上溯到Ext Class Loader和BootStrap Class Loader中去查找需要加载的类
由系统加载类来加载的应该是标准类和共同使用的类,Ext Class Loader和BootStrap Class Loader加载的是JDK标准类,AppClassLoader加载的是非JDK标准类之外的共同类