1. 双亲委派模型
1.1 什么是双亲委派模型
首先,先要知道什么是类的加载器。简单说,类加载器就是根据指定全限定名称将class文件加载到JVM内存,装维Class对象。如果站在JVM的角度来说,只存在两种加载器:
- 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在
\lib目录或-Xbootclasspath参数指定路径中的类库加载到内存中。 - 其他类型加载器:由Java语言实现,继承自抽象类ClassLoader。如:
- 扩展类加载器(Extension ClassLoader):负责加载
\lib\ext目录或java.ext.dirs系统便令指定的路径中的所有类库。 - 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。
双亲委派模型工作过程:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载中这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在及其的搜索范围内找不到指定的类时(即ClassNotFoundException),子加载器才会尝试自己去加载。
1.2 为什么需要双亲委派模型?
为什么需要双亲委派模型呢?假设没有双亲委派模型,试想一个场景:
黑客自定义一个
java.lang.String
类,该String
类具有系统的String
类一样的功能,只是在某个函数稍作修改。比如equals
函数,这个函数经常使用,如果在这个函数中,黑客加入一些“病毒代码”。并通过自定义类加载器加入到JVM
中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String
类是系统的String
类,导致“病毒代码”被执行。
而有了双亲委派模型,黑客自定义的java.lang.String
永远不会被加载到内存中。因为首先是最顶端的类加载器加载系统的java.lang.String
类,最终自定义的类加载器无法加载java.lang.String类。
或许你会想,我在定义的类加载器里面强制加载自定义的java.lang.String
类,不去通过调用父加载器不就好了吗?的确,这样是可行的。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与带比较类型的类加载器不同,那么会返回false。
举个栗子:
ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1,、Class2对象。那么Class1对象不属于ClassLoader2对象加载的java.lang.String类型。
2. 自定义类加载器
2.1 几个重要函数
loadClass默认实现如下:
public Class> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
再看看loadClass(String name, boolean resolve) 函数:
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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;
}
}
从上面代码可以明显看出,loadClass(String,boolean)函数即实现了双亲委派模型,整个大致过程如下:
- 首先,检查下制定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
- 如果此类没有加载过,那么,再判断下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.ladClass(name,false);)或者是调用
bootstrap
类加载器来加载。 - 如果父加载器及
bootstrap
类加载器都没有找到指定的类,那么调用当前类加载器的findClass
方法来完成类加载。
换句话说,如果自定义类加载器,就必须重写findClass方法!
find Class
findClass的默认实现如下
protected Class> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以看出,抽象类ClassLoader的findClass函数默认是抛出异常的。而前面我们知道,loadClass在父加载器无法加载类的时候,就会降低用我们自定义的类加载器中的findClass函数,因此我们必须在loadClass这个函数里面实现将一个指定类名称转换为class对象。
如果是读取一个指定的名称的类为字节数组的话,这个很好办。但是如何将字节数组转为class对象呢?很简单,Java提供了defineClass方法,通过这个方法,就可以把一个字节数组转为Class对象了。
defineClass
defineClass主要的功能是:
将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件时加密过的,则需要解密后作为形参传入defineClass函数。
defineClass默认实现如下:
protected final Class> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError {
return defineClass(name, b, off, len, null);
}