概述
Java的类加载,就是把字节码格式“.class”文件加载到JVM的方法区,并在JVM的堆区建立一个java.lang.Class对象的实例,用来封装Java类相关的数据和方法。都知道Tomcat打破了Java的双亲委托机制,本节就来对比下JVM类加载器和Tomcat类加载器;
一、JVM类加载器
-
BootstrapClassLoader
是启动类加载器,由C语言实现,用来加载JVM启动时所需要的核心类,比如rt.jar、resources.jar等。 -
ExtClassLoader
是扩展类加载器,用来加载\jre\lib\ext目录下JAR包。 -
AppClassLoader
是系统类加载器,用来加载classpath下的类,应用程序默认用它来加载类。 -
CustomClassLoader
自定义类加载器,用来加载自定义路径下的类。
这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说findClass查找的路径不同。双亲委托机制是为了保证一个Java类在JVM中是唯一的,假如你不小心写了一个与JRE核心类同名的类,比如Object类,双亲委托机制能保证加载的是JRE里的那个Object类,而不是你写的Object类。这是因为AppClassLoader在加载你的Object类时,会委托给ExtClassLoader去加载,而ExtClassLoader又会委托给BootstrapClassLoader,BootstrapClassLoader发现自己已经加载过了Object类,会直接返回,不会去加载你写的Object类。
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) {
// 如果父加载器没加载成功,调用自己的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;
}
}
protected Class<?> findClass(String name){
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class⽂件读入内存
...
//2. 调⽤defineClass将字节数组转成Class对象
return defineClass(buf, off, len);
}
// 将字节码数组解析成Class对象,native法实现
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
......
}
}
- JVM的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个parent字段,指向父加载器。
- defineClass是个工具方法,它的职责是调用native方法把Java类的字节码解析成一个Class对象,所谓的native方法就是由C语言实现的方法,Java通过JNI机制调用。
- findClass方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用defineClass方法得到Class对象。
- loadClass是个public方法,说明它才是对外提供服务的接口,具体实现也比较清晰:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个Java类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索Java类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。
二、Tomcat类加载器
-
WebAppClassLoader
Tomcat为给每个Web应用创建一个WebAppClassLoader类加载器。一个Context容器组件对应一个Web应用,因此,每个Context容器负责创建和维护一个WebAppClassLoader加载器实例。这背后的原理是,不同的加载器实例加载的类被认为是不同的类,即使它们的类名相同。这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间,每一个Web应用都有自己的类空间,Web应用之间通过各自的类加载器互相隔离。
这个类加载器在StandardContext.startInternal()
中被构造:
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
-
SharedClassLoader
作为WebAppClassLoader的父加载器,专门来加载Web应用之间共享的类。如果WebAppClassLoader自己没有加载到某个类,就会委托父加载器SharedClassLoader去加载这个类,SharedClassLoader会在指定目录下加载共享类,之后返回给WebAppClassLoader,这样共享的问题就解决了。 -
CatalinaClassloader
专门来加载Tomcat自身的类。这样设计有个问题,那Tomcat和各Web应用之间需要共享一些类时该怎么办呢? -
CommonClassLoader
老办法,还是再增加一个CommonClassLoader,作为CatalinaClassloader和SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader 使用,而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
shared.loader和server.loader在catalina.properties中配置默认为空,而且通常我们一个Tomcat容器只部署一个应用,因此SharedClassLoader和CatalinaClassloader不会使用到;
关于Tomcat类加载器层次的维护代码在Bootstrap.initClassLoaders
中,这里不做分析,下面分析下WebAppClassLoader
是如何打破双亲委派的
public synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> clazz = null;
// 1. 先在本地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. 通过系统的来加载器加载此类
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
//4. 判断是否需要委托给父类加载器进行加载
boolean delegateLoad = delegate || filter(name);
// 5. false不执行
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = Class.forName(name, false, loader);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
// 6. 在本地Web应用目录下查找并加载
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) {
// Ignore
}
// 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) {
// Ignore
}
}
throw new ClassNotFoundException(name);
}
-
1.
首先从当前ClassLoader的本地缓存中加载类,如果找到则返回。 -
2.
在本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。 -
3.
用ExtClassLoader去加载,这一步比较关键,目的防止Web应用自己的类覆盖JRE的核心类。因为Tomcat需要打破双亲委托机制,假如Web应用里自定义了一个叫Object的类,如果先加载这个Object类,就会覆盖JRE里面的那个Object类,这就是为什么Tomcat的类加载器会优先尝试用ExtClassLoader去加载,因为ExtClassLoader会委托给BootstrapClassLoader去加载,BootstrapClassLoader发现自己已经加载了Object类,直接返回给Tomcat的类加载器,这样Tomcat的类加载器就不会去加载Web应用下的Object类了,也就避免了覆盖JRE核心类的问题。 -
4.
判断是否需要委托给父类加载器进行加载,delegate属性默认为false,那么delegatedLoad的值就取决于filter的返回值了,filter方法中根据包名来判断是否需要进行委托加载,默认情况下会返回false。因此delegatedLoad为false。 -
5.
delegatedLoad为false,那么此时不会委托父加载器去加载; -
6.
如果ExtClassLoader加载器加载失败,也就是说JRE核心类中没有这类,那么就在本地Web应用目录下查找并加载。 -
7.
如果还是没有加载到类,并且不采用委托机制的话,则通过父类加载器去加载。
从上面的过程我们可以看到,Tomcat的类加载器打破了双亲委托机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖JRE的核心类,先尝试用JVM扩展类加载器ExtClassLoader去加载。
-------over-------