Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【JVM】浅谈双亲委派和破坏双亲委派

【JVM】浅谈双亲委派和破坏双亲委派

作者头像
joemsu
发布于 2018-08-03 06:22:58
发布于 2018-08-03 06:22:58
1.5K00
代码可运行
举报
文章被收录于专栏:皮皮之路皮皮之路
运行总次数:0
代码可运行

一、前言

笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自己读的有多粗糙,也体会到只有实践才能加深理解,正应对了那句话——“Talk is cheap, show me the code”。前段时间,笔者同事提出了一个关于类加载器破坏双亲委派的问题,以我们常见到的数据库驱动Driver为例,为什么要实现破坏双亲委派,下面一起来重温一下。

二、双亲委派

想要知道为什么要破坏双亲委派,就要先从什么是双亲委派说起,在此之前,我们先要了解一些概念:

  • 对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性

什么意思呢?我们知道,判断一个类是否相同,通常用equals()方法,isInstance()方法和isAssignableFrom()方法。来判断,对于同一个类,如果没有采用相同的类加载器来加载,在调用的时候,会产生意想不到的结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DifferentClassLoaderTest {

    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream stream = getClass().getResourceAsStream(fileName);
                if (stream == null) {
                    return super.loadClass(name);
                }
                try {
                    byte[] b = new byte[stream.available()];
                    // 将流写入字节数组b中
                    stream.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return super.loadClass(name);
            }
        };
        Object obj = classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof DifferentClassLoaderTest);

    }
}

输出结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class jvm.DifferentClassLoaderTest
false

如果在通过classLoader实例化的使用,直接转化成DifferentClassLoaderTest对象:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
DifferentClassLoaderTest obj = (DifferentClassLoaderTest) classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();

就会直接报java.lang.ClassCastException:,因为两者不属于同一类加载器加载,所以不能转化!

2.1、为什么需要双亲委派

基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

这里有几个流程要注意一下:

  1. 子类先委托父类加载
  2. 父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
  3. 子类在收到父类无法加载的时候,才会自己去加载

jvm提供了三种系统加载器:

  1. 启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载/lib下的类。
  2. 扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载/lib/ext下的类。
  3. 系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。

附上三者的关系:

2.2、双亲委派的实现

双亲委派的实现其实并不复杂,其实就是一个递归,我们一起来看一下ClassLoader里的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
                    // 前面提到,bootstrap classloader的类加载器为null,通过find方法来获得
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // 如果还是没有获得该类,调用findClass找到类
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // jvm统计
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            // 连接类
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

三、破坏双亲委派

3.1、为什么需要破坏双亲委派?

因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

3.2、破坏双亲委派的实现

我们结合Driver来看一下在spi(Service Provider Inteface)中如何实现破坏双亲委派。

先从DriverManager开始看,平时我们通过DriverManager来获取数据库的Connection:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root"); 

在调用DriverManager的时候,会先初始化类,调用其中的静态块:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
    ...
        // 加载Driver的实现类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                return null;
            }
        });
    ...
}

为了节约空间,笔者省略了一部分的代码,重点来看一下ServiceLoader.load(Driver.class)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static <S> ServiceLoader<S> load(Class<S> service) {
    // 获取当前线程中的上下文类加载器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

可以看到,load方法调用获取了当前线程中的上下文类加载器,那么上下文类加载器放的是什么加载器呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public Launcher() {
    ...
    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    Thread.currentThread().setContextClassLoader(this.loader);
    ...
}

sun.misc.Launcher中,我们找到了答案,在Launcher初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,而这个AppClassLoader,就是之前上文提到的系统类加载器Application ClassLoader,所以上下文类加载器默认情况下就是系统加载器

继续来看下ServiceLoader.load(service, cl)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // ClassLoader.getSystemClassLoader()返回的也是系统类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

上面这段就不解释了,比较简单,然后就是看LazyIterator迭代器:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private class LazyIterator implements Iterator<S>{
    // ServiceLoader的iterator()方法最后调用的是这个迭代器里的next
    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        // 根据名字来加载类
        try {
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service,
                 "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service,
                 "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service,
                 "Provider " + cn + " could not be instantiated",
                 x);
        }
        throw new Error();          // This cannot happen
    }
    
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    
    
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                // 在classpath下查找META-INF/services/java.sql.Driver名字的文件夹
                // private static final String PREFIX = "META-INF/services/";
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

}

好了,这里基本就差不多完成整个流程了,一起走一遍:

四、总结

Driver剩余的加载过程就省略了,有兴趣的园友可以继续深入了解一下,不得不说,jvm博大精深,看起来容易,真正到了用起来才发现各种问题,也只有实践才能加深理解,最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!

参考部分:

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018-07-14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Node.js 中使用 ES6 中的 import / export 的方法大全
Node.js 中使用 ES6 中的 import / export 的方法大全
一个会写诗的程序员
2018/12/06
5.3K0
Node.js 中使用 ES6 中的  import / export 的方法大全
Node.js中CommonJS和ECMAScript有什么区别?
Node.js 既支持 CommonJS 标准,也完全支持 ECMAScript 标准。Node.js 环境下用 js语言编写的文件,有三种格式:.js、.mjs、.cjs。
Learn-anything.cn
2021/11/26
1.1K0
Node.js 22 正式发布,支持 Require() ESM 模块!
Node.js 22 将于十月进入长期支持(LTS)阶段,但在那之前,它将在接下来的六个月内作为“当前”发布版本。我们鼓励您探索此最新版本提供的新功能和优势,并评估它们对您的应用程序的潜在影响。
ConardLi
2024/04/28
5440
Node.js 22 正式发布,支持 Require() ESM 模块!
一杯茶的时间,上手 Node.js
Node.js 太火了,火到几乎所有前端工程师都想学,几乎所有后端工程师也想学。一说到 Node.js,我们马上就会想到“异步”、“事件驱动”、“非阻塞”、“性能优良”这几个特点,但是你真的理解这些词的含义吗?这篇教程将带你快速入门 Node.js,为后续的前端学习或是 Node.js 进阶打下坚实的基础。
一只图雀
2020/04/07
1.1K0
【云+社区年度征文】webpack 学习笔记系列02-模块化开发
三大 JavaScript 主流模块规范:CommonJS、AMD 和 ES6 Module。CommonJS 和 AMD 都未统一浏览器和客户端的模块化规范。目前 Node.js 使用 CommonJS 作为官方的模块解决方案,虽然内置的模块方案促进了 Node.js 的流行,但是也为引入新的 ES Modules(ESM)标准造成了一定的阻碍,不过 Node.js 9.0+ 已经支持 ESM 语法。
CS逍遥剑仙
2020/12/23
1.2K0
【架构师(第九篇)】如何让 Node 环境支持 ES Module
运行程序,发现会报错 Cannot use import statement outside a module ,意思就是不让用 import 语法。
一尾流莺
2022/12/10
1.1K0
【架构师(第九篇)】如何让 Node 环境支持 ES Module
深入了解“前端模块化”发展体系
作为一名前端工程师,每天的清晨,你走进公司的大门,回味着前台妹子的笑容,摘下耳机,泡上一杯茶,打开 Terminal 进入对应的项目目录下,然后 npm run start / dev 或者 yarn start / dev 就开始了一天的工作。
小生方勤
2019/05/31
7560
Node.js 在 2020 年有什么新东西
2019 年,Node.js 已经10岁了,而 NPM 上可用的包数量也超过了 100 万个。Node.js 本身的下载数也仍在上升,同比上年增长 40%。另一个重要的里程碑是 Node.js 最近加入了 OpenJS 基金会,该基金会承诺改善项目的健康度和可持续性,并加强与整个 JavaScript 社区的协作。
coder_koala
2020/03/11
1.3K0
前端模块化规范
完整高频题库仓库地址:https://github.com/hzfe/awesome-interview
HZFEStudio
2021/09/13
7770
Node新版本13.2.0正式支持ES Modules特性
在本月 21 日,即2019.11.21,Node.js 发布了 13.2.0 版本,更新了一些特性。其中最令人兴奋的莫过于正式取消了 --experimental-modules 启动参数。这说明Node.js 正式支持 ES modules。我们一起来看看。
winty
2019/12/20
1.5K0
Node.js宣布新的--experimental-modules【译】
原文:Announcing a new --experimental-modules
ACK
2020/01/14
1.8K0
前端模块化的今生
众所周知,早期 JavaScript 原生并不支持模块化,直到 2015 年,TC39 发布 ES6,其中有一个规范就是 ES modules(为了方便表述,后面统一简称 ESM)。但是在 ES6 规范提出前,就已经存在了一些模块化方案,比如 CommonJS(in Node.js)、AMD。ESM 与这些规范的共同点就是都支持导入(import)和导出(export)语法,只是其行为的关键词也一些差异。
ConardLi
2019/12/17
7040
Javascript模块化详解
前端的发展日新月异,前端工程的复杂度也不可同日而语。原始的开发方式,随着项目复杂度提高,代码量越来越多,所需加载的文件也越来越多,这个时候就需要考虑如下几个问题:
Clearlove
2021/03/11
6290
Javascript模块化详解
node.js基础入门
node.js是一个基于Google V8引擎的、跨平台的JavaScript运行环境,不是一个语言
黄啊码
2022/06/20
7990
Node.js的模块解析机制
Node.js的模块解析机制基于CommonJS规范,该规范定义了如何在JavaScript中实现模块功能。在Node.js中,每个文件都被视为一个独立的模块,拥有自己的作用域。模块之间通过require()函数来引入依赖,并通过exports或module.exports来导出模块成员。
jack.yang
2025/04/05
1060
2020 年 Node.js 将会有哪些新功能[每日前端夜话0xFA]
2019 年是 Node.js 诞生的第 10 个年头,npm 上可用的包数量超过了 100 万。Node.js 本身的下载量也在持续增长,同比增长了 40%。另一个重要的里程碑是 最近 Node.js加入了 OpenJS 基金会,该基金会承诺改善项目的状况和可持续性,并改善与整个 JavaScript 社区的协作。
疯狂的技术宅
2019/12/12
1.1K0
2020 年 Node.js 将会有哪些新功能[每日前端夜话0xFA]
前端必知之:前端模块化的CommonJS规范和ES Module规范详解
这种写法很容易存在全局污染和依赖管理混乱问题。在多人开发前端应用的情况下问题更加明显。
肥晨
2024/08/09
3120
前端模块化-CommonJS,AMD,CMD,ES6
随着 JavaScript 工程越来越大,团队协作不可避免,为了更好地对代码进行管理和测试,模块化的概念逐渐引入前端。模块化可以降低协同开发的成本,减少代码量,同时也是“高内聚,低耦合”的基础。
李振
2021/11/26
4240
[译] What's New for Node.js in 2020
Node.js在2019年走到了第十个年头, npm上面的包数量也超过了一百万. NodeJS自身的下载量也在以每年40%的速度持续增长. 而对于NodeJS最近的另一个里程碑便是它加入了OpenJS基金会, 该基金会旨在提高项目的健康度与可持续性, 同时与JavaScript社区有一个紧密的合作.
腾讯IVWEB团队
2020/06/28
2K0
node.js笔记
4、语法: 1)加载 path 模块 2)使用 path.join 方法,拼接路径
打不着的大喇叭
2024/03/11
1720
node.js笔记
推荐阅读
相关推荐
Node.js 中使用 ES6 中的 import / export 的方法大全
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验