1、JVM的类加载器
1)、类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等
这里的相等包括代表类的Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括使用instanceof
关键字做对象所属关系判定等情况
2)、什么是双亲委派模型
- 启动类加载器(Bootstrap ClassLoader):负责将放在
<JAVA HOME>\lib
目录中的,或者被-Xbootclasspath
参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可 - 扩展类加载器(ExtClassLoader):由
sun.misc.Launcher$ExtClassLoader
实现,它负责加载<JAVA HOME>\lib\ext
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器 - 应用程序类加载器(AppClassLoader):由
sun.misc.Launcher$AppClassLoader
实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()
方法的返回值,所以一般也称它为系统类加载。它负责加载用户类路径(ClassPath)上所有指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器
类加载之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码
双亲委派模型的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object
,它存放在rt.jar
之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
3)、java.lang.ClassLoader
JDK提供一个抽象类java.lang.ClassLoader
,核心方法源码如下:
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
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) {
//先委托给父加载器去加载,注意这是个递归调用
c = parent.loadClass(name, false);
} else {
//如果父加载器为空,查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
long t1 = System.nanoTime();
//如果父加载器没加载成功,调用自己的findClass去加载
c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
//根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读到内存得到字节码数组,最终将字节数组转成Class对象
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
- 每个类加载器都持有一个parent字段,指向父加载器。类加载器的父子关系不是通过继承来实现的,比如AppClassLoader并不是ExtClassLoader的子类,而是说AppClassLoader的parent成员变量指向ExtClassLoader对象
findClass()
方法主要职责就是找到.class
文件读到内存得到字节码数组,最终将字节数组转成Class对象loadClass()
方法是实现双亲委派模型的关键:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载
2、Tomcat如何打破双亲委派模型
Tomcat的自定义类加载器WebAppClassLoader打破了双亲委派模型,它首先自己尝试去加载某个类,如果找不到再代理给父类加载器,其目的是优先加载Web应用自己定义的类。具体实现就是WebappClassLoader的父类WebappClassLoaderBase中重写了ClassLoader的两个方法:findClass()
和loadClass()
findClass()
方法源码如下:
public abstract class WebappClassLoaderBase extends URLClassLoader
implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug(" findClass(" + name + ")");
checkStateForClassLoading(name);
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
if (log.isTraceEnabled())
log.trace(" securityManager.checkPackageDefinition");
securityManager.checkPackageDefinition(name.substring(0,i));
} catch (Exception se) {
if (log.isTraceEnabled())
log.trace(" -->Exception-->ClassNotFoundException", se);
throw new ClassNotFoundException(name, se);
}
}
}
Class<?> clazz = null;
try {
if (log.isTraceEnabled())
log.trace(" findClassInternal(" + name + ")");
try {
if (securityManager != null) {
PrivilegedAction<Class<?>> dp =
new PrivilegedFindClassByName(name);
clazz = AccessController.doPrivileged(dp);
} else {
//1.先在Web应用目录下查找类
clazz = findClassInternal(name);
}
} catch(AccessControlException ace) {
log.warn(sm.getString("webappClassLoader.securityException", name,
ace.getMessage()), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
if ((clazz == null) && hasExternalRepositories) {
try {
//2.如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
} catch(AccessControlException ace) {
log.warn(sm.getString("webappClassLoader.securityException", name,
ace.getMessage()), ace);
throw new ClassNotFoundException(name, ace);
} catch (RuntimeException e) {
if (log.isTraceEnabled())
log.trace(" -->RuntimeException Rethrown", e);
throw e;
}
}
//3.如果父类也没找到,抛出ClassNotFoundException
if (clazz == null) {
if (log.isDebugEnabled())
log.debug(" --> Returning ClassNotFoundException");
throw new ClassNotFoundException(name);
}
} catch (ClassNotFoundException e) {
if (log.isTraceEnabled())
log.trace(" --> Passing on ClassNotFoundException");
throw e;
}
if (log.isTraceEnabled())
log.debug(" Returning class " + clazz);
if (log.isTraceEnabled()) {
ClassLoader cl;
if (Globals.IS_SECURITY_ENABLED){
cl = AccessController.doPrivileged(
new PrivilegedGetClassLoader(clazz));
} else {
cl = clazz.getClassLoader();
}
log.debug(" Loaded by " + cl.toString());
}
return clazz;
}
在findClass()
方法里,主要有三个步骤:
- 先在Web应用本地目录下查找要加载的类
- 如果没有找到,交给父加载器去查找,它的父加载器就是应用程序类加载器(AppClassLoader)
- 如果父加载器也没找到这个类,抛出ClassNotFound异常
loadClass()
方法源码如下:
public abstract class WebappClassLoaderBase extends URLClassLoader
implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck {
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class<?> clazz = null;
checkStateForClassLoading(name);
//1.先在当前ClassLoader的本地cache中查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
//2.调用ClassLoader的findLoadedClass方法查看JVM是否已经加载过此类
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
//3.通过启动类加载器(Bootstrap ClassLoader)加载此类,这里防止应用写的类覆盖了JavaSE的类
String resourceName = binaryNameToPath(name, false);
ClassLoader javaseLoader = getJavaseClassLoader();
boolean tryLoadingFromJavaseLoader;
try {
URL url;
if (securityManager != null) {
PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
url = AccessController.doPrivileged(dp);
} else {
url = javaseLoader.getResource(resourceName);
}
tryLoadingFromJavaseLoader = (url != null);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
tryLoadingFromJavaseLoader = true;
}
if (tryLoadingFromJavaseLoader) {
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
}
}
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = sm.getString("webappClassLoader.restrictedPackage", name);
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
//4.判断是否需要委托给父类加载器进行加载
boolean delegateLoad = delegate || filter(name, true);
//5.因为delegatedLoad为false,那么此时不会委托父加载器去加载
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
}
}
//6.尝试在本地目录搜索class并加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
}
//7.通过父类加载器去加载
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
}
}
}
//上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}
在loadClass()
方法里,主要有六个步骤:
- 先在当前ClassLoader的本地cache中查找该类是否已经加载过
- 调用ClassLoader的
findLoadedClass()
方法查看JVM是否已经加载过此类 - 通过启动类加载器(Bootstrap ClassLoader)加载此类,目的是防止Web应用自己的类覆盖JRE的核心类。因为Tomcat需要打破双亲委派模型,假如Web应用里自定义了一个叫Object的类,如果先加载这个Object类,就会覆盖JRE里面的那个Object类,这就是为什么Tomcat的类加载器会优先尝试用Bootstrap ClassLoader去加载,因为Bootstrap ClassLoader发现自己已经加载了Object类,直接返回给Tomcat的类加载器,这样Tomcat的类加载器就不会去加载Web应用下的Object类了,也就避免了覆盖JRE核心类的问题
- 如果Bootstrap ClassLoader加载器加载失败,也就是说JRE核心类中没有这类,那么就在本地Web应用目录下查找并加载
- 如果本地目录下没有这个类,说明不是Web应用自己定义的类,那么由应用程序类加载器(AppClassLoader)去加载。Web应用是通过
Class.forName
调用交给AppClassLoader的,因为Class.forName
的默认加载器就是AppClassLoader - 如果上述加载过程全部失败,抛出ClassNotFound异常
这里第三步是通过启动类加载器(Bootstrap ClassLoader)加载此类,也有的文章说是通过扩展类加载器(ExtClassLoader),Tomcat 8.5x版本给javaseClassLoader赋值代码如下:
public abstract class WebappClassLoaderBase extends URLClassLoader implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck { protected WebappClassLoaderBase() { super(new URL[0]); ClassLoader p = getParent(); if (p == null) { p = getSystemClassLoader(); } this.parent = p; ClassLoader j = String.class.getClassLoader(); if (j == null) { j = getSystemClassLoader(); while (j.getParent() != null) { j = j.getParent(); } } this.javaseClassLoader = j; securityManager = System.getSecurityManager(); if (securityManager != null) { refreshPolicy(); } }
可以看到javaseClassLoader和加载String的ClassLoader相同,而且javaseClassLoader的父加载器为null,所以为Bootstrap ClassLoader
Tomcat的类加载器双亲委派模型的目的是为了优先加载Web应用目录下的类,然后再加载其他目录下的类,这也为实现多个Web应用的隔离奠定了基础。这也符合Servlet规范的建议:全路径类名与系统类同名的话,优先加载Web应用自己定义的类
如果你并不想打破双亲委派模型,但是又想定义自己的类加载器来加载特定目录下的类,你需要重写
findClass()
和loadClass()
方法中的哪一个?还是两个都要重写?答:只需要重写
findClass()
方法,findClass()
方法和loadClass()
方法区别在于:findClass()
方法负责具体的类加载细节,主要职责就是找到.class
文件,可能来自文件系统或者网络,找到后把.class
文件读到内存得到字节码数组,然后调用defineClass()
方法得到Class对象。而loadClass()
方法只是保证双亲委派机制运行流程,先看当前类是否被加载,若没有没加载交由父级类加载器加载,父类加载器加载失败由当前类加载器加载
3、Tomcat如何实现多个Web应用的隔离
Tomcat作为一个Web服务器需要解决以下问题:
- 多个Web应用隔离类库:部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一个类库在一个服务器中只有一份,服务器应当可以保证两个应用程序的类库可以互相独立使用
- 多个Web应用共享类库:部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。例如用户可能有10个使用Spring的应用程序部署在同一台服务器上,如果把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——类库在使用时都要被加载到服务器内存,如果类库不能共享,虚拟机的方法区很容易就会出现过度膨胀的风险
- 隔离Tomcat与Web应用的类:Tomcat服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响,服务器所使用的类库应该与应用程序的类库互相独立
1)、Tomcat类加载器的层次结构
为了解决这些问题,Tomcat 设计了类加载器的层次结构,它们的关系如下图:
WebAppClassLoader:
为了解决多个Web应用隔离类库的问题,Tomcat自定义了一个类加载器WebAppClassLoader,并且给每个Web应用创建一个WebAppClassLoader加载器实例,因此,每个Context容器负责创建和维护一个WebAppClassLoader加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间,每一个Web应用都有自己的类空间,Web应用之间通过各自的类加载器互相隔离
SharedClassLoader:
多个Web应用共享类库的问题本质是多个Web应用之间共享类库并且不能重复加载相同的类。在双亲委派模型里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗。SharedClassLoader作为WebAppClassLoader的父加载器专门来加载Web应用之间共享的类。如果WebAppClassLoader自己没有加载到某个类,就会委托父加载器SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给WebAppClassLoader,来解决共享的问题
CatalinaClassLoader:
如何隔离Tomcat与Web应用的类?要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此Tomcat又设计一个类加载器CatalinaClassLoader专门来加载Tomcat自身的类
CommonClassLoader:
如果Tomcat和各Web应用之间需要共享一些类时该怎么办呢?CommonClassLoader作为CatalinaClassLoader和SharedClassLoader的父加载器
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离
2)、Spring的加载问题
在JVM的实现中有一条隐含的规则,默认情况下,如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载。比如Spring作为一个Bean工厂,它需要创建业务类的实例,并且在创建业务类实例之前需要加载这些类。Spring是通过调用Class.forName
来加载业务类的
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
可以看到在forName()
的函数里,会用调用者也就是Spring的加载器去加载业务类
前面讲到Web应用之间共享的类库可以交给SharedClassLoader来加载,从而避免重复加载。如果Spring作为共享的第三方类库,它本身是由SharedClassLoader来加载的,Spring又要去加载业务类,按照前面的规则,加载Spring的类加载器也会用来加载业务类,但是业务类在Web应用目录下,应该由WebAppClassLoader来加载而不是SharedClassLoader,这该怎么办?
线程上下文加载器是一种类加载器传递机制,这个类加载器保存在线程私有数据里,只要是同一个线程,一旦设置了线程上下文加载器,在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器,并在启动Web应用的线程里设置线程上下文加载器,这样Spring在启动时就将线程上下文加载器取出来,用来加载Bean。Spring取线程上下文加载的代码如下:
cl = Thread.currentThread().getContextClassLoader();
Spring运行时懒加载业务bean,请求线程会触发依赖对象注入到IOC容器。因为应用程序执行的宿主环境就是Web容器,请求线程的类加载器自然就是WebAppClassLoader,Spring-Core包下
ClassUtils.getDefaultClassLoader()
代码如下:public abstract class ClassUtils { public static ClassLoader getDefaultClassLoader() { ClassLoader cl = null; try { cl = Thread.currentThread().getContextClassLoader(); } catch (Throwable ex) { } if (cl == null) { cl = ClassUtils.class.getClassLoader(); if (cl == null) { try { cl = ClassLoader.getSystemClassLoader(); } catch (Throwable ex) { } } } return cl; }
容器启动时,Spring注入Bean时的类加载器还是SharedClassLoader。初始加载bean和懒加载bean的驱动线程不同,一个是Spring应用程序自身,一个是业务线程
参考:
https://time.geekbang.org/column/article/105110
https://time.geekbang.org/column/article/105711