Java服务器端编程JVM · Java虚拟机原理 · JVM上语言·框架· 生态系统Java 核心技术

JVM类加载和双亲委派机制

2022-04-22  本文已影响0人  pq217

类加载器

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

类和类库

jvm运行时的类主要分三种

三种不同的类存储在磁盘不同的目录下,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实现自己的一套隔离版的类加载机制

上一篇下一篇

猜你喜欢

热点阅读