月薪过万必会的:双亲委托模型

2022-10-11,,,,

类加载器简介

在介绍双亲委托模型之前,先介绍一下类加载器。类加载器通过一个类的全限定名来转换为描述这个类的二进制字节流。

对于任意一个类,被同一个类加载器加载后都是唯一的,但如果被不同加载器加载后,就不是唯一的了。即使是源于同一个class文件、被同一个jvm加载,只要加载类的加载器不同,那么类就不同。

如何判断类是否相同,可以使用class对象的equals()方法、isassignablefrom()方法、isinstance()方法的返回结果进行判断,也可以使用instanceof关键字进行对象所属关系的判断。
下面我们写一个不同类加载器加载后的类,看一下对instanceof关键字运算有什么影响:

public class onemorestudy {
    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 inputstream = getclass().getresourceasstream(filename);
                    if (inputstream == null) {
                        return super.loadclass(name);
                    }
                    byte[] array = new byte[inputstream.available()];
                    inputstream.read(array);
                    return defineclass(name, array, 0, array.length);
                } catch (ioexception e) {
                    throw new classnotfoundexception(name);
                }
            }
        };
        object object = myloader.loadclass("onemorestudy").newinstance();

        system.out.println("class name: " + object.getclass().getname());
        system.out.println("instanceof: " + (object instanceof onemorestudy));
    }
}

运行结果:

class name: onemorestudy
instanceof: false

在运行结果中,第一行可以看出这个对象确实是onemorestudy类实例化出来的,但在第二行中instanceof运算结果是false,说明在jvm中存在两个onemorestudy类,一个是由系统应用程序类加载器加载的,另一个是由我们自定义的类加载器加载的。虽然都是来自同一个class文件,在同一个jvm里,但是被不同的类加载器加载后,仍然是两个独立的类。

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

类加载器的划分

除了像上面例子代码中,我们自己实现的自定义类加载器,还有3种系统提供的类加载器:

  1. 启动类加载器(bootstrap classloader):它负责将存放在%java_home%\lib目录中的,或者被-xbootclasspath参数所指定的路径中的,并且是jvm识别的类库加载到jvm内存中。它仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载。它是由c++语言实现的,无法被java程序直接引用。

  2. 扩展类加载器(extension classloader):它负责加载%java_home%\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。它由sun.misc.launcher.extclassloader实现,开发者可以直接使用扩展类加载器。

  3. 应用程序类加载器(application classloader):它负责加载用户类路径(classpath)上所指定的类库。由于它是classloader中的getsystemclassloader()方法的返回值,所以一般也称它为系统类加载器。它由sun.misc.launcher.appclassloader来实现,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

双亲委托模型

之前提到,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在jvm中的唯一性。可是有这么多种的类加载器,如何保证一个类在jvm中的唯一性呢?为了解决这个问题,双亲委托模型(parents delegation model)应运而生,它就是下图所展示的类加载器之间的层次关系:

除了顶层的启动类加载器外,其余的类加载器都必须有自己的父类加载器。类加载器之间的父子关系,一般不会以继承的关系来实现,而是都使用组合关系来复用父类加载器。

类加载器收到类加载的请求后,它不会首先自己去尝试加载这个类,而是把这个请求委派给父类加载器去尝试加载。每一个类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中。只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。这样就保证了类在jvm中的唯一性,也保证了java程序稳定运作。

实现双亲委派模型的代码都集中在java.lang.classloader的loadclass()方法之中,如下:

protected class<?> loadclass(string name, boolean resolve)
    throws classnotfoundexception
{
    synchronized (getclassloadinglock(name)) {
        //首先,检查该类是否已经被加载过了
        class<?> c = findloadedclass(name);
        //如果没有加载过,就调用父类加载器的loadclass()方法
        if (c == null) {
            long t0 = system.nanotime();
            try {
                if (parent != null) {
                    c = parent.loadclass(name, false);
                } else {
                    //如果父类加载器为空,就使用启动类加载器
                    c = findbootstrapclassornull(name);
                }
            } catch (classnotfoundexception e) {
                //如果在父类加载器中找不到该类,就会抛出classnotfoundexception
            }

            if (c == null) {
                //如果父类找不到,就调用findclass()来找到该类。
                long t1 = system.nanotime();
                c = findclass(name);
                
                //记录统计数据
                sun.misc.perfcounter.getparentdelegationtime().addtime(t1 - t0);
                sun.misc.perfcounter.getfindclasstime().addelapsedtimefrom(t1);
                sun.misc.perfcounter.getfindclasses().increment();
            }
        }
        if (resolve) {
            resolveclass(c);
        }
        return c;
    }
}

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是java设计者们推荐给开发者们的类加载器实现方式。大部分的类加载器都遵循这个模型,但也有例外的情况,比如下面这三种情况:

重写classloader的loadclass()方法

在上面例子代码中,就是重写了classloader的loadclass()方法,破坏了双亲委派模型,产生了不唯一的类。所以,不提倡开发人员覆盖loadclass()方法,而应当把自己的类加载逻辑写到findclass()方法中,在loadclass()方法的逻辑里如果父类加载失败,则会调用自己的findclass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型。

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

spi(服务提供者接口)

java提供了很多spi(service provider interface,服务提供者接口),允许第三方为这些接口提供实现,常见的spi有jdbc、jndi、jce、jaxb和jbi等。

spi的接口由java核心库来提供,而这些spi的实现代码则是作为java应用所依赖的jar包被包含进类路径(classpath)里。spi接口中的代码经常需要加载具体的实现类。那么问题来了,spi的接口是java核心库的一部分,是由启动类加载器来加载的;spi的实现类是由系统类加载器来加载的。引导类加载器是无法找到spi的实现类的,因为依照双亲委派模型,启动类加载器无法委派系统类加载器来加载类。

这时候就会使用线程上下文类加载器(thread context classloader),在jvm中会把当前线程的类加载器加载不到的类交给线程上下文类加载器来加载,直接使用thread.currentthread().getcontextclassloader()来获得,默认返回的就是应用程序类加载器,也可以通过java.lang.thread类的setcontextclassloader()方法进行设置。

而线程上下文类加载器破坏了双亲委派模型,也就是父类加载器请求子类加载器去完成类加载的动作,但为了实现功能,这也是一种巧妙的实现方式。

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

osgi(开放服务网关协议)

osgi(open service gateway initiative,开放服务网关协议)技术是面向java动态化模块化系统模型,程序模块(称为bundle)无需重新引导可以被远程安装、启动、升级和卸载。实现程序模块热部署的关键则是它自定义的类加载器机制的实现。

在osgi中,类加载器不再是双亲委派模型中的树状结构,而是一个较为复杂的网状结构,类加载的规则简要介绍如下:

  1. 若类属于java.*包,则将加载请求委托给父加载器
  2. 若类定义在启动委托列表(org.osgi.framework.bootdelegation)中,则将加载请求委托给父加载器
  3. 若类属于在import-package中定义的包,则框架通过classloader依赖关系图找到导出此包的bundle的classloader,并将加载请求委托给此classloader
  4. 若类资源属于在require-bundle中定义的bundle,则框架通过classloader依赖关系图找到此bundle的classloader,将加载请求委托给此classloader
  5. bundle搜索自己的类资源( 包括bundle-classpath里面定义的类路径和属于bundle的fragment的类资源)
  6. 若类在dynamicimport-package中定义,则开始尝试在运行环境中寻找符合条件的bundle

如果在经过上面一系列步骤后,仍然没有正确地加载到类资源,则会向外抛出类未发现异常。

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

总结

类加载器通过一个类的全限定名来转换为描述这个类的二进制字节流,可划分为启动类加载器扩展类加载器应用程序类加载器自定义类加载器。在双亲委托模型中,将上述各种类加载器组成一系列的父子关系,子类加载器先把类加载请求委派给父类加载器去尝试加载,父类加载器无法加载时子类加载器才自己尝试加载,这样保证了类在jvm中的唯一性。不过,也不遵循双亲委托模型的情况,比如:重写classloader的loadclass()方法、spi(服务提供者接口)、osgi(开放服务网关协议)。

欢迎关注微信公众号:万猫学社,每周一分享java技术干货。

《月薪过万必会的:双亲委托模型.doc》

下载本文的Word格式文档,以方便收藏与打印。