JVM类加载器及双亲委派模型

1.前言

前面讲解了类的加载机制,对于JVM类的加载过程有了简单的了解,这一章接着学习类加载的一些细节,类加载器和双亲委派模型

2.目录

目录

3.类加载器

在JVM中有三类ClassLoader构成:启动类(或根类)加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用类加载器(AppClassLoader).不同类加载器负责加载不同区域的类

类加载器
  • 启动类加载器:这个加载器不是一个Java类,而是由底层的c++实现,负责将存放在JAVA_HOME下lib目录中的类库,比如rt.jar.因此,启动类加载器不属于Java类库,无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可
  • 扩展类加载器:由sun.misc.Launcher$ExtClassLoader实现,负责加载JAVA_HOME下libext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器
  • 应用类加载器:由sun.misc.Launcher$AppClassLoader实现的.由于这个类加载器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系统类加载器.它负责加载用户类路径上所指定的类库,可以被直接使用.如果未自定义类加载器,默认为该类加载器

3.1.类加载器的初始化

除启动类加载器外,扩展类加载器和应用类加载器都是通过类sun.misc.Launcher进行初始化,而Launcher类则由根类加载器进行加载.相关代码如下:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //初始化扩展类加载器,构造函数没有入参,无法获取启动类加载器
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }
    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);
    ...
}

4.双亲委派模型

双亲委派模型:当一个类加载器接收到类加载请求时,会先请求其父类加载器加载,依次递归,当父类加载器无法找到该类时(根据类的全限定名称),子类加载器才会尝试去加载

双亲委派模型

loadClass源码:
ClassLoader类是一个抽象类,但却没有包含任何抽象方法.继承ClassLoader类并重写findClass方法便可实现自定义类加载器.但如果破坏上面所述的双亲委派模型来实现自定义类加载器,则需要继承ClassLoader类并重写loadClass方法和findClass方法

ClassLoader的源码如下:

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) {
                    //如果当前类没有被加载且父类加载器不为null,则请求父类加载器进行加载操作
                    c = parent.loadClass(name, false);
                } else {
                   //如果当前类没有被加载且父类加载器为null,则请求根类加载器进行加载操作
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
               //如果父类加载器加载失败,则由当前类加载器进行加载,
                c = findClass(name);
                //进行一些统计操作
               // ...
            }
        }
        //初始化该类
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

使用双亲委派模型的原因:
双亲委派模型是为了保证Java核心库的类型安全,所有Java应用都至少需要引用java.lang.Object类,在运行时这个类需要被加载到Java虚拟机中.如果该加载过程由自定义类加载器来完成,可能就会存在多个版本的java.lang.Object类,而且这些类之间是不兼容的.通过双亲委派模型,对于Java核心库的类的加载工作由启动类加载器来统一完成,保证了Java应用所使用的都是同一个版本的Java核心库的类,是互相兼容的

4.1.上下文类加载器

子类加载器都保留了父类加载器的引用,但如果父类加载器加载的类需要访问子类加载器加载的类该如何处理?最经典的场景就是JDBC的加载
JDBC是Java制定的一套访问数据库的标准接口,它包含在Java基础类库中,由根类加载器加载.而各个数据库厂商的实现类库是作为第三方依赖引入使用的,这部分实现类库是由应用类加载器进行加载的

连接MySql

//加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//连接数据库
Connection conn = DriverManager.getConnection(url, user, password);

DriverManager由启动类加载器加载,它使用到的数据库驱动com.mysql.jdbc.Driver是由应用类加载器加载的,这就是典型的由父类加载器加载的类需要访问由子类加载器加载的类

DriverManager类连接的实现源码:

//建立数据库连接底层方法
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
    //获取调用者的类加载器
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        //由启动类加载器加载的类,该值为null,使用上下文类加载器
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }
    //...
    for(DriverInfo aDriver : registeredDrivers) {
        //使用上下文类加载器去加载驱动
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                //加载成功,则进行连接
                Connection con = aDriver.driver.connect(url, info);
                //...
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        } 
        //...
    }
}
callerCL = Thread.currentThread().getContextClassLoader();

这行代码从当前线程中获取ContextClassLoader,而ContextClassLoader在哪里设置呢?就是在上面的Launcher源码中设置的

// 设置上下文类加载器
Thread.currentThread().setContextClassLoader(this.loader);

这样一来,所谓的上下文类加载器本质上就是应用类加载器.因此,上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并不是一个全新的类加载器,本质上是应用类加载器

4.2.自定义类加载器

自定义类加载器只需要继承java.lang.ClassLoader类,然后重写findClass(String name)方法即可,在方法中指明如何获取类的字节码流

如果要破坏双亲委派规范的话,还需重写loadClass方法(双亲委派的具体逻辑实现).但不建议这么做。

public class TestClassLoader extends ClassLoader {
    private String mClassPath;
    public TestClassLoader() {
    }
    public TestClassLoader(String classPath) {
        mClassPath = classPath;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name,classData,0,classData.length);
        }
    }
    private byte[] getClassData(String name) {
        String path = convert2Path(name);
        try {
            FileInputStream fis = new FileInputStream(path);
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] bytes = new byte[1024];
            int length = 0;
            while ((length = fis.read(bytes)) != -1) {
                byteArrayOutputStream.write(bytes,0,length);
            }
            return byteArrayOutputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
    private String convert2Path(String name) {
        return mClassPath + File.separatorChar
                + name.replace('.', File.separatorChar) + ".class";
    }
}

main()中测试TestClassLoader

public static void main(String[] args) {
    String testPath = Test.class.getProtectionDomain().getCodeSource().getLocation().getFile();
    System.out.println(testPath);
    TestClassLoader testClassLoader = new TestClassLoader(testPath);
    Class<?> aClass;
    try {
        aClass = testClassLoader.loadClass(TestClassLoader.class.getPackage().getName() + "." + TestClassLoader.class.getSimpleName());
        System.out.println(aClass.newInstance().toString());
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    }
}
打印结果

上面的代码通过重写了findClass获取class的路径便实现了自定义的类加载器,
当JDK提供的类加载器实现无法满足我们的需求时,才需要自己实现类加载器。比如,插件化和热更新

5.总结

这一章主要讲解了类加载器和双亲委派模型,其实理解起来也并不复杂,但是在实际的场景中,必须对其机制有足够的了解,才可以实现插件化和热更新类似的需求,这也是android中的一大黑科技


原文地址:
https://www.choupangxia.com/2019/10/29/interview-jvm-gc-04/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352