JVM类加载和双亲委派机制

类加载器

类加载器的作用就是把磁盘中的类文件加载到内存的方法区以供使用,分析类加载前,先看下jvm运行时都需要加载什么样的类

类和类库

jvm运行时的类主要分三种

  • 核心类
    比如String,Thread,Lock等,这种类都是jdk提供的,我们不需要自己写,路径在JRE的lib目录下
  • 扩展类
    一些jdk提供的扩展类,比如DESKey等加密的类,也不需要自己写,路径是JRE的lib目录下的ext扩展目录
  • 应用程序类
    也就是程序员自己写的类

三种不同的类存储在磁盘不同的目录下,jvm运行时需要类加载器把类信息加载到内存,具体说是工作区中,而针对三种类型的类分别是三种不同的类加载器负责加载的

引导类加载器/核心类加载器

jvm启动时首先会创建一个引导类加载器实例,是由C++实现的,它的作用就是加载支撑JVM运行时的核心类库,这些类库所在地就是我们jdk安装路径下的jre/lib中的一些jar包

jre/lib

其中最核心的就是rt.jar,我们使用的大部分基础类比如:String,Thread都定义在这个包下

扩展类加载器

借助C++,有了引导类加载器,就可以实现加载一些基本的类,同时也可以定义一个自己的(java)的类加载器的基本类,由引导类加载器加载到工作区,就可以创建它的实例,用这个实例就可自己去加载其他的类:额外类和应用程序类,就不用再麻烦C++实现的引导类加载器,毕竟java就可以做了
这就好比鸡生蛋问题,一个农夫要吃鸡蛋,先从外部买个鸡,下了鸡蛋又可生成鸡继续下蛋,就不需要再去买鸡了

rt.jar类库中还有一个重量级的类:sun.misc.Launcher,翻译过来就是启动器,jvm在启动时通过C++实现的引导类加载器加载了这个类,然后生成一个该类的实例,实例初始化的构造方法如下

Launcher

其中Launcher.ExtClassLoader.getExtClassLoader()
就是初始化一个扩展类加载器实例,它可以把JRE的lib目录下的ext扩展目录下的类库加载到工作区

应用程序类加载器

而下面的Launcher.AppClassLoader.getAppClassLoader(var1)就是初始化一个应用程序类加载器实例,它可以把用户代码加载到工作区

解释

这里可能有点蒙,可以用下面例子帮助理解一下

我们把jvm比作一个造万物的工厂,工厂可以生产任何实物(对象实例),前提是必须有图纸(class)才能创建出来

工厂工作时为了查找方便把生产出的实物和图纸分开存储,其中图纸存入工作方法区,实物存入工作堆区

工厂向外部提供服务,客户给一张图纸,工厂就可以生产出实物

同时工厂提供一些基本组件的图纸(核心类),客户可以在自己的图纸中标志使用这些基本组件

工厂提供一些额外组件的图纸(扩展类),满足特殊需求,客户也可以在自己的图纸中标志使用这些额外组件

但是客户给的图纸都放在一个图纸收集栏中(java.class.path),工厂需要收集栏图纸加载到工作方法区,并做一些特殊处理。与此同时工厂提供的基本组件图纸和额外图纸(放在仓库里)也需要首先加载到工作方法区,这样才能在实际生产客户实物用到时使用

为了实现加载这个加载过程,工厂首先从其它工厂借了个引导加载器(C++实现),它的工作就是把基本组件图纸从仓库加载到工作方法区

有了这个引导加载器,只能加载基本组件图纸,客户提交的图纸和额外图纸还是无法加载

于是工厂自己生成加载器,在基本组件图纸仓库中中制作了“额外图纸加载器”和“客户图纸加载器”的图纸

工厂开始工作后,有了引导类加载器的加载,就拿到了两张自定义加载器的图纸,然后创建这个“额外图纸加载器”和“客户图纸加载器”的实物,

然后用“额外图纸加载器”可以把额外的图纸加载进工作方法区

再用“客户图纸加载器”可以把客户提交的图纸加载到工作方法区, 就这样所有生产所需的图纸都可以获取到了,便可实现生产

查看类加载器

有了这三种类加载器,我们程序所需的类都可以加载到工作区,进而生成类的实例,我们可以使用代码一探这些加载器

public class TestJDKClassLoader {
    public static void main(String[] args) {
            System.out.println(String.class.getClassLoader()); // String的类加载器
            System.out.println(DESKeyFactory.class.getClassLoader().getClass().getName());
            System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
            System.out.println(ClassLoader.getSystemClassLoader());
    }
}

输出

null
sun.misc.Launcher$ExtClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$AppClassLoader

可以看到String.class的类加载器是null,因为引导类加载器是C++的对象,所以java打印不出来
DESKeyFactory.class在ext包下,所以它的类加载器是ExtClassLoader(扩展类加载器)
TestJDKClassLoader是自己实现的类,对应加载器AppClassLoader(应用类加载器)
另外一种获取类加载器的方式ClassLoader.getSystemClassLoader()

类加载时机

上文一直说类加载“可以”加载,那么实际上这些类什么时候被加载呐,可以做个测试

这里补充一下static代码块的代码是类加载后执行的

public class TestDynamicLoad {

    static {
        System.out.println("*************load TestDynamicLoad************");
    }

    public static void main(String[] args) {
        new A();
        B b = null;  //B不会加载,除非这里执行 new B()
    }
}

class A {
    static {
        System.out.println("*************load A************");
    }

    public A() {
        System.out.println("*************initial A************");
    }
}

class B {
    static {
        System.out.println("*************load B************");
    }

    public B() {
        System.out.println("*************initial B************");
    }
}

运行结果:
*************load TestDynamicLoad************
*************load A************
*************initial A************

可以看到Class B并没有被实际加载,说明类加载也是一种懒加载,用到才使用类加载器加载

双亲委派机制

层级结构

上文介绍了三种不同的类加载器分别加载不同类型的类,其实他们仨除了分工不同,还有上下级的关系,来看一下ClassLoader(类加载器的抽象)源码

ClassLoader

一个类加载器中会包含一个parent属性指向父类加载器,形成了一个单项链表,而以上三种类加载器的父子结构是这样的

结构

类加载机制

有了这个层级有什么用呐,这就涉及到类加载机制,也就是双亲委派机制,机制其实很简单:因为每个类加载器负责的类地址不一样,所以当一个类加载器想加载一个类时先让父节点去父地盘找,找不到子节点再找,递归下去最终结果就是,要加载一个类,依次去引导类加载器>扩展类加载器>应用类加载器寻找,找到后就加载,看一下ClassLoader.loadClass代码

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
        synchronized (getClassLoadingLock(name)) {
            // 0.检查是否已加载,如果是就不用重新加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) { // 1.1 如果父节点不是null,让父节点先查
                        c = parent.loadClass(name, false);
                    } else { //1.2 如果父节点是null,即引导类加载器,去查找基础类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) { //2. 父类查不到,自己找
                    // If still not found, then invoke findClass in order to find the class.
                    long t1 = System.nanoTime();
                    // 自己找的方法
                    c = findClass(name);
                    ...                

示意图如下

双亲委派机制

双亲委派机制的作用

以上介绍了双亲委派机制,那为什么要这么设计,主要是防止核心api被篡改,比如你自己写一个String类且包名和原类一致,但由于双亲委派机制的存在,你写的永远不会被加载。

比如说上例的工厂已经提供了标准螺丝钉的设计图,如果客户意图使用自己的螺丝钉设计图替换工厂的是不行的

自定义类加载器

以上三种类加载器,除了引导类加载器,都是java实现的,那作为java程序员可不可以自己写一个类加载器?答案是肯定的,比如我们可以继承上文提到的ClassLoader抽象类,尝试写一个类加载器来把桌面上存放的的类加载进来

ClassLoader实际查找累的方法是findClass,也就是上文loadClass方法中当父节点差找不到类时所执行的方法,默认如下

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

默认抛出异常,也就是说要想实现类加载器,这个方法肯定要重写的,最终代码如下

public class MyClassLoader extends ClassLoader {
    /**
     * 桌面路径
     */
    private String classPath = "C:/Users/Administrator/Desktop";

    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

     /**
     * 重写findClass方法
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

测试一下,创建一个类放入桌面,包名是com,所以放一个com文件夹并放进去,并通过javac转换为class文件

package com;
public class MyClass {
    public void say() {
        System.out.println("hello");
    }
}
桌面建com包
存class文件

测试代码

public static void main(String[] args) throws Exception {
    MyClassLoader classLoader = new MyClassLoader();
    // 获取MyClass类
    Class clazz = classLoader.loadClass("com.MyClass");
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("say", null);
    method.invoke(obj, null);
    System.out.println(clazz.getClassLoader().getClass().getName()); // 输出hello
}

这样我们就实现了一个自定义类加载器,可以把各个地方的类文件加载进来并运行

如果打印自定义加载器的父加载器就是AppClassLoader,是因为ClassLoader抽象类中有个无参构造函数(子类实例化会调用父类无参构造)

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader()); // getSystemClassLoader()的结果就是AppClassLoader
}

因此用自定义加载器去加载String.class也行的通,因为双亲委派会去父级先找

打破双亲委派机制

“打破双亲委派机制”好像总被提起,听起来很高端,其实看代码双亲委派机制不过是对ClassLoader.loadClass方法执行过程起的一个名字,也就是说只是ClassLoader.loadClass实现了双亲委派机制,那作为子类完全可以覆盖重写,所以所谓打破,也不过就是子类覆盖重写了父类的默认代码而已

比如说ClassLoader.loadClass的逻辑是先去父级找,找不到再findClass,我们可以改成先findClass,找不到再调用父级的loadClass不就打破了吗

Tomcat

提到“打破双亲委派机制”,就不得不提Tomcat,因为它就是一个打破双亲委派机制的典型案例

Tomcat为什么要打破这个机制,主要因为一个tomcat容器可能同时运行多个项目,多项目可能都有一样报名和类名的类,但功能不一样,比如引入不同的版本的三方类库这种问题就太多了:

比如说项目一引用了1.0版本的某框架,项目二引用了同一框架的2.0版本,那么某类被加载一次就不会再被加载两个项目其实用到的都是同一版本的某类,这样肯定就错错了

所以Tomcat必须打破原机制,也就是重写loadClass实现自己的一套隔离版的类加载机制

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

推荐阅读更多精彩内容