第七章 虚拟机类加载机制 | part 2

21 阅读5分钟

类加载器

类的加载过程中实现 “通过一个类的全限定名来获取描述该类的二进制字节流” 这个动作的代码被称为 “类加载器”。

类与类加载器

对于任意一个类,必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机中的唯一性。如果不注意类加载器的影响,可能导致 instanceof 关键字运算的结果令人迷惑。例如在下面的代码中,第一个 ClassLoaderTest 是由自定义的加载器加载的,而另一个 ClassLoaderTest 是由应用程序类加载器加载的,因此虽然它们的全类名相同,结果却返回 false。

public class ClassLoaderTest {
    public static void main(String[] args) throws Exception {
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null) {
                        return super.loadClass(name);
                    }
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name, b, 0, b.length);
                }
                catch(IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = myLoader.loadClass("org.example.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());  //class org.example.ClassLoaderTest
        System.out.println(obj instanceof org.example.ClassLoaderTest);  //false
    }
}

双亲委派模型

上一节中有提到应用程序类加载器,那这是个什么呢?这就涉及到双亲委派模型了。在 JVM 的角度来看,存在两种不同的类加载器:一种是启动类加载器,由 C++ 实现,是虚拟机的一部分;另一种是其他所有的类加载器,都由 Java 实现,且全部继承自抽象类 java.lang.ClassLoader

事实上,自 JDK1.2 以来,Java 一直保持着三层类加载器、双亲委派的类加载架构,这种架构如图所示。

image.png
  • 启动类加载器

    这个类加载器负责加载存放在 <JAVA_HOME>\lib 目录下,并且能被 JVM 识别的类库。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器去处理,直接使用 null 代替即可。

  • 扩展类加载器

    这个类加载器负责加载 <JAVA_HOME>\lib\ext目录下,或者被 java.ext.dirs 系统变量所指定的路径中所有的类库。扩展类加载器直接由 Java 代码实现,因此可以在程序中直接使用它来加载 Class 文件。

  • 应用程序类加载器

    应用程序类加载器也被称为系统类加载器,负责加载用户类路径上所有的类库,开发者同样可以直接在代码中使用这个类加载器。

接下来就轮到本节的重点内容 —— 双亲委派模型闪亮登场啦!双亲委派模型的流程如下:

  1. 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
  2. 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
  3. 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
  4. 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。

双亲委派模型显而易见的好处是 Java 中的类随着它的类加载器一起具备了一种带有优先级的层次关系,例如类 java.lang.Object,它存在于 rt.jar 中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此如果用户自定义 Object 类,那么在应用程序加载器试图加载这个类的时候,会发现父类加载器已经加载过了,从而放弃加载。

双亲委派模型对于 Java 程序的稳定运行极为重要,但实现却异常简单:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    //首先,检查该类是否已经加载过
    Class c = findLoadedClass(name);
    if (c == null) {
        //如果 c 为 null,则说明该类没有被加载过
        try {
            if (parent != null) {
                //当父类的加载器不为空,则通过父类的loadClass来加载该类
                c = parent.loadClass(name, false);
            } else {
                //当父类的加载器为空,则调用启动类加载器来加载该类
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            //非空父类的类加载器无法找到相应的类,则抛出异常
        }

        if (c == null) {
            //当父类加载器无法加载时,则调用findClass方法来加载该类
            //用户可通过覆写该方法,来自定义类加载器
            long t1 = System.nanoTime();
            c = findClass(name);
        }
    }
    if (resolve) {
        //对类进行link操作
        resolveClass(c);
    }
    return c;
}

破坏双亲委派模型

从上面双亲委派模型的执行流程可以看出,如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法,无法被父类加载器加载的类最终会通过这个方法被加载;如果想打破双亲委派模型则需要重写 loadClass() 方法,因为 loadClass() 就是实现双亲委派逻辑的地方。

Tomcat 是一个典型的打破双亲委派模型的例子。我们知道一个 Tomcat 可以部署多个 Web 应用程序,假设现在有两个 Web 应用程序,它们都有一个叫做 User 的类,并且拥有相同的全类名,为了保证两者不冲突,Tomcat 给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了 loadClass 方法,优先加载当前应用目录下的类,只有当前找不到了才一层一层往上找。