前面介绍了类加载的几个过程,实际中这些过程大部分都是由虚拟机本身去执行的,我们没有办法去改变或影响这些过程的执行。但是虚拟机团队将类加载阶段第一步中的”通过一个类的全限定名来获取描述该类的二进制字节流”这个动作放到虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块被称为”类加载器”。
1.类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段,对于任何一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。更通俗的来说:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。下面先来分析下类加载器的主要方法,
ClassLoader这个抽象类代表了典型的内加载器逻辑,通过分析它的源码可以看到里面主要有下面几个比较重要的方法:
方法 | 作用 |
---|---|
loadClass() | 通过传入的全限定类名拿到类对应的二进制字节流然后生成为类生成一个Class对象,这个方法里的逻辑体现了双亲委托模型 |
defineClass() | 接受类的二进制字节流,为类生成一个Class对象,是通过调用native方法实现的 |
findLoadedClass() | 接受需要加载类的名称,通过检查是否存在对应的Class对象来检测类是否已经被加载,这里通过类的全限定名获取Class对象,里面调用的是native方法findLoadedClass0()。 |
findClass() | 在loadClass()中执行完双亲委托查找模型之后,如果仍然没有找到所需要的类,则会调用findClass()方法来进行查找,所以在实现自己的类加载器的时候可以通过重写这个方法实现。 |
主要的类加载逻辑还是在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) {
c = parent.loadClass(name, false);
} else {
//否则调用native方法findBootstrapClass进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
//如果上述步骤没有找到Class文件,则使用findClass()进行加载,在ClassLoader中这个方法是没有具体实现的
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;
}
}
如果不深入通过全限定类名查找Class文件和解析二进制数据流得到类的Class对象的过程,整个类加载的过程还是挺简单的:先判断该类是否已经被加载过了,如果没有被加载并且当前加载器有父加载器,则调用父加载器的loadClass()方法来重复这个过程,如果没有父加载器,则意味着当前的加载器是启动类加载器,调用findBootstrapClass的native方法尝试进行加载。如果上面的过程都没有加载成功,则通过findClass()方法尝试进行加载。
下面通过自定义一个类加载器来验证前面提到的类加载器和Class文件确定一个类唯一性的结论:
public class MyClassLoader {
public static void main(String[] args) throws Exception {
final ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if (inputStream == null) {
return super.loadClass(name);
}
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new ClassNotFoundException();
}
}
};
Object obj = classLoader.loadClass("com.sankuai.lkl.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof com.sankuai.lkl.ClassLoaderTest);
}
}
基本的逻辑就是通过自定义的类加载器加载一个类获得Class对象,然后通过newInstance()方法获得这个类的一个实例,并且与系统应用程序类加载器加载的类进行类型比较。输出结果如下,可以看到虽然来自同一个Class文件但确实被看成了两个不同的类:
class com.sankuai.lkl.ClassLoaderTest
false
2.双亲委派模型
上面介绍ClassLoader的loadClass()源码的时候就可以看到类加载器在加载一个类的时候是会先委托给它的父加载器进行加载的,这其实就是双亲委托模型,在介绍双亲委托模型更具体的内容之前先来看下类加载器的分类。
从虚拟机的角度来说,有两类不同的类加载器:一种是启动类加载器(BootStrap ClassLoader),使用C++实现是虚拟机的一部分,外部也没法显式调用这个类加载器;
另外一种是所有其他的类加载器,这些加载器都是使用Java语言实现的,独立于虚拟机外部,这些类加载器都继承自java.lang.ClassLoader。如果要更细致的分,可以将类加载器分类以下三类:
名称 | 作用 |
---|---|
启动类加载器(Bootstrap ClassLoader) | 这个类加载器负责将存放在<JAVA_HOME>/lib 目录或-Xbootclasspath参数所指定的路径中的,并且是被虚拟机识别的类库加载到内存中,启动类加载器是无法被Java程序直接引用的 |
扩展类加载器(Extension ClassLoader) | 这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>/lib/ext 目录中或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 |
应用程序类加载器(Application ClassLoader) | 这个类加载器由sun.misc.Lancher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称之为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义自己的类加载器,则这个加载器就是程序中默认的类加载器。 |
除了这三类加载器,我们还可以定义自己的类加载器,这些类加载器是相互配合工作的,他们大致的关系如下图:
这里展示的层次关系其实就是双亲委托模型,在这个模型里面除了最顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,这里的父子关系一般是通过组合实现的。
双亲委托模型其实是通过ClassLoader的loadClass()方法实现的,上面已经看过它的源码了,这里在归纳下它大致的工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父加载器进行加载,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己进行加载(在findClass()中实现自己查找的逻辑)。
最后需要注意的是双亲委托模型只是一种约定而已,并非强制性的约束;所以在很多情况会对其进行破坏来实现一些特别的功能。