类加载器与双亲委派机制


类加载器分类

类加载器主要有以下几种:

类加载器 加载范围
启动类加载器(Bootstrap ClassLoader) 1. 加载%JAVA_HOME%/lib下的jar包
2. _Xbootclasspath参数指定路径的jar(不常用)
注意:由C++实现,不是ClassLoader的子类,只加载虚拟机认可的类,手动把包放到lib目录下是不管用的
扩展类加载器(Extension ClassLoader) 1. 加载%JAVA_HOME%/lib/ext下的jar包
2. java.ext.dirs系统变量指定的路径的jar
应用类加载器(App ClassLoader) 用户classpath指定的类,也就是我们日常开发的类
自定义类加载器(User ClassLoader) 上面的类加载器无法满足需求时,就自定义类加载器

确定两个类是否相同

在确定两个类是否相同时,有一个大坑需要我们注意。

每一个类加载器都有各自的类名称空间,例如我们编写的某个类A,正常情况会被App ClassLoader加载,此时如果我们自定义一个类加载器,也去加载A的class文件,也能加载成功。

如果用instanceof去判断这两个类是否是同一个类,你将得到一个false的结果,即使这两个类其实是一模一样的。

也就是说,判断两个类是否是同一个类,前提是这两个类必须被同一个类加载进行加载,否则没有意义。

双亲委派机制

从JDK1.2开始,引入了双亲委派机制,这是JAVA设计者推荐给开发者的一种类加载器的实现方式。

双亲委派机制的大致过程是:

  • 当一个类加载器要加载类时,不会直接进行加载,而是把这个请求交给它的父类加载器。同样的,父类加载器同样也会将请求往上传递,层层传递直到到达顶层。
  • 顶层类加载器会尝试加载这个类,如果成功,则结束,如果失败,则让下层类加载器尝试加载,如果再失败,继续层层往下传递,直到没有成功或者没有下层

整个过程大致如下图所示:

为什么要用这种机制去加载一个类呢?

例如类java.lang.Object,是在rt.jar包中,使用双亲委派机制,可以确保Object类一定会被最顶层的启动类加载器加载,因为下层即使收到加载Object类的请求,也会把请求往上委托,最终被启动类加载器加载完成。

否则程序中就可能出现多个java.lang.Object,将一片混乱

破坏双亲委派机制

双亲委派机制并不是一种强制性的模型要求,因此在JDK发展历史上出现了三次较大规模的破坏

第一次破坏

双亲委派机制是在JDK1.2开始出现,其核心代码在ClassLoader.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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    // 加载类
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    // 删除部分代码
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

在JDK1.2之前,就已经有自定义类加载器的能力,开发者们继承ClassLoader类并重写loadClass()方法来自定义类加载器。

而双亲委派的核心代码都在loadClass()方法中,一旦被开发者重写,双亲委派机制就被破坏了。

因此在双亲委派机制出现之后,增加了findClass()方法,建议开发者们在自定义类加载时,选择重写findClass()而不是loadClass(),避免对双亲委派进行破坏。

因此,可以说双亲委派历史上第一次较大规模的破坏,发生在双亲委派出现之前

第二次破坏

第二次破坏的出现,是由于双亲委派机制的缺陷导致。

有些情况下,顶层的BootstrapClassLoader,需要使用到classpath下的类,也就是需要调用AppClassLoader去加载类,也就破坏了双亲委派模型。

示意图如下:

什么时候会出现这种情况呢?

以JDBC为例,rt.jar中有一个接口java.sql.Driver,该接口由BootstrapClassLoader进行加载。

但SQL的驱动的实现类是由服务提供商提供(如MySQL服务商),而提供商提供的实现类com.mysql.jdbc.Driver是在classpath下的,由AppClassLoader进行加载。

Java设计了SPI(Service Provider Interface)机制,约定在jar包的META-INF/services/目录下的文本文件中,存放了服务实现类的全限定类名,也就是com.mysql.jdbc.Driver

如下图:

有了SPI机制之后,BootstrapClassLoader就能够通过AppClassLoader加载com.mysql.jdbc.Driver类,但这种行为破坏了双亲委派模型。

那么BootstrapClassLoader是如何拿到AppClassLoader的呢?

其实很简单,Java启动时,会将AppClassLoader放到当前线程的上下文中,BootstrapClassLoader就可以直接从线程上下文中拿到了。

代码如下:

try {
    loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
    throw new InternalError(
        "Could not create application class loader", e);
}

// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);

第三次破坏

第三次破坏是因为开发对“动态性”的追求,比如热部署。简单来说就是希望JVM能够再不重启的前提下,动态更新代码。

OSGi(Open Service Gateway Initiative) 技术是 Java 动态化模块化系统的一系列规范,该技术自定了类加载器,有着自己的类加载机制,它是一种网状结构的类加载方式,破坏了双亲委派机制,可以实现热部署。

鼎鼎大名的Eclipse就是基于OSGi开发的

文章作者: 周君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 周君 !
评论