Java 类加载机制与双亲委派

本文详细介绍了Java的类加载机制,包括加载、验证、准备、解析、初始化、使用和卸载等步骤。重点讨论了双亲委派模型,解释了为何设计这一模型的原因,如沙箱安全机制和避免类重复加载。同时,通过示例展示了如何自定义类加载器以打破双亲委派,以及在Tomcat中类加载器如何实现应用隔离。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Java 类加载机制与双亲委派

Java 类加载机制

其中loadClass的类加载过程有如下几步:

在这里插入图片描述

加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

  • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口
  • 验证:校验字节码文件的正确性
  • 准备:给类的静态变量分配内存,并赋予默认值
  • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下 节课会讲到动态链接
  • 初始化:对类的静态变量初始化为指定的值,执行静态代码块类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的 引用、对应class实例的引用等信息。 类加载器的引用:这个类到类加载器实例的引用 对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

类在运行过程中如果使用到其它类,会逐步加载这些类。


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

    public static void main(String[] args) {
        new A();
        System.out.println("*************load test************");
        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************");
    }

}

用到那个类就加载那个类
在这里插入图片描述

类加载器

   //jdk自带核心类
        System.out.println(String.class.getClassLoader());
        //扩展类 ext包下
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());

        //自定义的类
        System.out.println(TestJDKClassLoader.class.getClassLoader().getClass().getName());
        System.out.println();
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        System.out.println("the bootstrapLoader : " + bootstrapLoader);
        System.out.println("the extClassloader : " + extClassloader);
        System.out.println("the appClassLoader : " + appClassLoader);
        System.out.println();
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);

        }
        System.out.println();
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println();
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));

执行结果
在这里插入图片描述

可以看到

sun.misc.LauncherExtClassLoadersun.misc.LauncherExtClassLoader sun.misc.LauncherExtClassLoadersun.misc.LauncherAppClassLoader

可以看到默认的类加载器是appClassLoader, 父类设置了ExtClassLoader
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

可以看到 AppClassLoader 和ExtClassLoader 父类 UrlClassLoader , 父类 ClassLoader
在这里插入图片描述

ExtClassLoader 父类 Null
在这里插入图片描述

Java里有如下几种类加载器

  • 引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
  • 扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那 些类
  • 自定义加载器:负责加载用户自定义路径下的类包,这个由我们自己去实现

双亲委派

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。

在这里插入图片描述

可以看到这里首先判断类是否被加载过,有直接返回
没有就先判断是否有父类加载器,有就父类加载器加载
没有就bootstarp加载器加载
还没有就叫给子加载器加载,加载到就返回
在这里插入图片描述
没有加载到依次子类加载

在这里插入图片描述

为什么要设计双亲委派机制?

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改

  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

自己定义一个 String 类,可以看到根本运行不了,甚至idea 编译代码这里 都没有出现run main 的标志

在这里插入图片描述

自定义类加载器

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空 方法,所以我们自定义类加载器主要是重写findClass方法。

package jvm;


import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MyClassLoader extends ClassLoader {

    String path;
    public MyClassLoader( String path){
        this.path=path;
    }

    private byte[] loadByte() throws Exception {
        ///Users/pilgrim/Desktop/User1.class
        FileInputStream fis = new FileInputStream(path);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;

    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] data = new byte[0];
        try {
            data = loadByte();
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
        return defineClass(name, data, 0, data.length);
    }


    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {


        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
        MyClassLoader classLoader = new MyClassLoader("/Users/pilgrim/Desktop/User1.class");
        //我们自己盘创建 jvm 目录,将User类的复制类User1.class丢入该目录
        Class clazz = classLoader.loadClass("jvm.User1");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("sout", null);
        method.setAccessible(true);
        method.invoke(obj);
        System.out.println(clazz.getClassLoader().getClass().getName());
    }
}

结果
注意一定要在自己目录把 User1的class文件删除了。不然就是app ClassLoader 去加载了。
在这里插入图片描述

不同版本的 User1 类文件

这里将 User1 改一下 表示不同的版本
在这里插入图片描述

然后放到test目录下

用两个类加载器来加载,加载执行的结果是第一个 User1 版本,类已经被加载过了就不会加载其他的版本了,类的全限定命名是一样的。

在这里插入图片描述

对于这种情况我们应该怎么办,写 2 个不同类名的 User1,User2。

但是jar 依赖都是一个名,只是版本不一样,对此,我们需要打破这种委派。

打破双亲委派机制

如果是我们自己写的类 我们自己加载 不委派给父类。

package jvm.myclassloader;


import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 打破双亲委派 *
 */
public class MyClassLoader2 extends ClassLoader {

    String path;

    public MyClassLoader2(String path) {
        this.path = path;
    }

    private byte[] loadByte() throws Exception {
        ///Users/pilgrim/Desktop/User1.class
        FileInputStream fis = new FileInputStream(path);
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;

    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                //是我们自己的类 自己加载 不是让父类加载
                if (name.startsWith("jvm")) {
                    c = findClass(name);
                } else {
                    c = this.getParent().loadClass(name);
                }
            }
            return c;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte[] data = new byte[0];
        try {
            data = loadByte();
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
        return defineClass(name, data, 0, data.length);
    }


    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {


        MyClassLoader2 classLoader1 = new MyClassLoader2("/Users/pilgrim/Desktop/User1.class");
        //我们自己盘创建 jvm 目录,将User类的复制类User1.class丢入该目录
        Class clazz1 = classLoader1.loadClass("jvm.User1");
        Object obj1 = clazz1.newInstance();
        Method method1 = clazz1.getDeclaredMethod("sout", null);
        method1.setAccessible(true);
        method1.invoke(obj1);
        System.out.println(clazz1.getClassLoader().getClass().getName());


        //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
        MyClassLoader2 classLoader2 = new MyClassLoader2("/Users/pilgrim/Desktop/test/User1.class");
        //我们自己盘创建 jvm 目录,将User类的复制类User1.class丢入该目录
        Class clazz2 = classLoader2.loadClass("jvm.User1");
        Object obj2 = clazz2.newInstance();
        Method method = clazz2.getDeclaredMethod("sout", null);
        method.setAccessible(true);
        method.invoke(obj2);
        System.out.println(clazz2.getClassLoader().getClass().getName());


    }
}

可以看到,不同的类加载器加载执行的类结果是不一样的,也就是说不同的版本的类,相同的名,被分别加载了。

在这里插入图片描述

同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一 样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类 加载器也是同一个才能认为他们是同一个。

Tomcat打破双亲委派机制

其实这种就是 Tomcat 类加载器的原型

  • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。

  • 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。 默认的类加载器是能够实现的,因为他的职责就是保证唯一性

  • jsp文件的热加载,可以直接卸载掉这jsp文件的类加载器,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载 器。重新创建类加载器,重新加载jsp文件。

在这里插入图片描述

tomcat的几个主要类加载器:

  • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容 器本身以及各个Webapp访问;

  • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不 可见;

  • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;

  • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前 Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的

  • WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;

每个 webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委 派机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TizzyGoodhealth

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值