深入理解 Java 虚拟机(七)类加载器

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011330638/article/details/82718254

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取此类的二进制字节流”的动作放到 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的模块称为“类加载器”。

类加载器最初是为了满足 Applet 的需求而开发的,但确在类层次划分、OSGi、热部署、代码加密等领域大放异彩,是 Java 技术体系中一块重要的基石。

类与类加载器

对于任意一个类,都需要由它的类加载器和这个类本身以通确定其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即,如果要比较两个类是否相等,只有在这两个类由同一个类加载器加载的前提下才有意义,否则,即使是同一个 Class 文件,位于同一个 Java 虚拟机中,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里的“相等”指的是 Class 对象返回的 equal()、isAssignableFrom()、isInstance() 方法返回的结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。如:

/**
* 类加载器与instanceof关键字演示
*
* @author zzm
*/
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.fenixsoft.classloading.ClassLoaderTest").newInstance();

        System.out.println(obj.getClass());
        System.out.println(obj instanceof org.fenixsoft.classloading.ClassLoaderTest);
    }
}

运行结果:

class org.fenixsoft.classloading.ClassLoaderTest
false

第二句返回 false 是因为虚拟机中存在两个 ClassLoaderTest 类,一个是系统应用程序类加载器加载的,一个是自定义的加载器加载的,虽然来自同一个 Class,但依然是两个独立的类。

双亲委派模型

从 Java 虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器,使用 C++ 实现,是虚拟机自身的一部分;另一种是其它所有的类加载器,这些加载器都由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。

从 Java 开发人员的角度讲,还可以划分得更细致一些,绝大部分 Java 程序都会用到以下 3 种系统提供的类加载器:

1) 启动类加载器(Bootstrap ClassLoader)。这个类加载器负责将存放在 < JAVA_HOME>/lib 目录下的,或者被 -Xbootclasspath 参数所指定的路径中的,且能被虚拟机识别的类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接返回 null 即可。比如 getClassLoader 的源码如下:

/**
* Returns the class loader for the class.  Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
*
*/
public ClassLoader getClassLoader() {
    ClassLoader cl = getClassLoader0();
    if (cl == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    }
    return cl;
}

2) 扩展类加载器(Extension ClassLoader),它负责加载 < JAVA_HOME>/lib/ext 目录中的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

3) 应用程序类加载器(Application ClassLoader),可通过 ClassLoader.getSystemClassLoader() 获取,也称为系统类加载器,它负责加载用户类路径(Class Path) 上所指定的类库,开发者可以直接使用这个类加载器,一般情况下它就是程序中默认的类加载器。

这些类加载器之间的关系如图:

类加载器

如图所示的类加载器之间的层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了启动类加载器之外,其余的类加载器都应该有自己的父类加载器,这里的父子关系一般通过组合来实现。但它不是强制性的约束模型,而是 Java 设计者推荐的一种实现方式。

双亲委派模型的工作过程是:如果一个类加载器受到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,因此所有的加载请求最终都会传送到顶层的启动类加载器中,只有当父类加载器无法完成这个请求时,子加载器才会尝试自己去加载。

使用双亲委派模型的好处是稳定,比如,无论哪个类加载器要加载 Object 类,最终都会使用启动类加载器加载,因此 Object 类在程序的各个类加载器环境中都是同一个类。它的实现很简单,代码都集中在 java.lang.ClassLoader 的 loadClass() 方法中:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // 首先,检查类是否已被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 如果没有加载过,则调用父加载器的 loadClass 方法
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 父加载器为空,则使用启动类加载器作为父加载器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果抛出 ClassNotFoundException 异常,说明父类加载器无法完成请求
            }

            if (c == null) {
                // 父类无法加载时,使用自身的 findClass 方法进行加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
}

破坏双亲委派模型

Java 的大部分类加载器都遵循双亲委派模型,但也有为了达成某个目的而被破坏的情况,这里的“破坏”不带贬义,相反,还可以认为是一种创新。目前为止,双亲委派模型主要出现过 3 此较大规模的被破坏情况。

双亲委派模型是 JDK 1.2 之后引入的,因此为了兼容以前的版本,做了一些妥协,这是双亲委派模型的第一次被破坏。JDK 1.2 之后提倡重写 findClass 方法而不是 loadClass 方法。

第二次被破坏的典型例子是 JNDI 服务。JNDI 服务使用线程上下文类加载器去加载所需要的 SPI(Service Provider Interface) 代码,即父类加载器请求子类加载器去完成类加载的动作。

第三次被破坏是由于用户对程序动态性的追求而导致的,动态指代码热替换、模块热部署等。在这方面,OSGi 已经成为了业界事实上的 Java 模块化标准,OSGi 实现模块热部署的关键是它自定义的类加载机制的实现:每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。

在 OSGi 环境下,类加载器不再是双亲委派的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:

1) 将以 java.* 开头的类委派给父类加载器加载

2) 否则,将委派列表名单内的类委派给父类加载器加载

3) 否则,将 import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载

4) 否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载

5) 否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载

6) 否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载

7) 否则,类查找失败

Java 程序员中基本有一个共识:OSGi 对类加载器的使用是很值得学习的,弄懂了 OSGi 的实现,就可以算是掌握了类加载器的精髓

总结

这里写图片描述

思维导图

猜你喜欢

转载自blog.csdn.net/u011330638/article/details/82718254