从类加载机制到双亲委派

2023年 10月 3日 21.4k 0

什么是类加载

每个编写出的.java文件都存储着需执行的程序逻辑,经过Java编译器编译,

会为每个.java文件生成对应的.class字节码文件,.class文件中则记录着Java代码转换之后的虚拟机指令,每个.class文件开头都有特定的标识、魔数版本等信息。

当JVM需要用到某个类时,虚拟机会加载它的.class文件,加载了相关的字节码信息后,会为它创建对应的Class对象,而这个过程就被称为类加载。

类加载的过程

类加载过程被分为三个步骤,五个阶段,分别为加载、验证、准备、解析以及初始化。加载、验证、准备、初始化这四个阶段的顺序是确定的。但解析阶段不一定,为了支持Java语言的运行时绑定特性,在某些情况下可以在初始化阶段之后再开始(也称为动态绑定或晚期绑定)

image.png

  • 加载

  • 通过完全限定名查找定位.class文件,并获取其二进制字节流数据
  • 把字节流所代表的静态存储结构转换为运行时数据结构
  • 在堆中间中为其创建一个Class对象,作为程序访问这些数据的入口
  • 验证(验证被加载的Class的正确性)

  • 文件格式验证:验证字节流是否复合Class文件的规范
  • 元数据验证:对字节码描述的信息进行语义的分析,以保证其描述符合Java语言的规范(语法检查)
  • 字节码验证:通过数据流和控制流分析,确定程序语义的合法性
  • 符号引用验证:确保后续的解析工作可以正确执行
  • 准备:为类中的静态变量分配内存空间(static成员),并完成数值的初始化(例如static int i = 5;这里只会将i初始化为0)

  • 解析:把类中对常量池内的符号引用转换为直接引用的过程

  • 符号引用:用一组符号来描述引用的目标,符号引用的字面量形式明确定义在==《Java虚拟机规范》==的Class文件格式中
  • 直接引用:直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄
  • 初始化:主要是对类的静态变量赋予正确的初始值,也就是在声明静态变量时指定的初始化值以及静态代码块中的赋值

  • JVM类加载器

    image.png

    • 启动类加载器(Bootstrap ClassLoader)用来加载java核心类库,无法被java程序直接引用。
    • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
    • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
    • 用户自定义类加载器,通过继承 java.lang.ClassLoader类的方式实现

    什么是双亲委派

    当某个类加载器需要加载某个.class文件时,它首先把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类

    image.png

    双亲委派的作用

    • ①防止加载同一个.class。通过委托去询问上级是否已经加载过该.class,如果加载过了,则不需要重新加载。保证了数据安全。
    • ②保证核心.class不被篡改。通过委托的方式,保证核心.class不被篡改,即使被篡改也不会被加载,即使被加载也不会是同一个class对象,因为不同的加载器加载同一个.class也不是同一个Class对象。这样则保证了Class的执行安全

    image.png

    双亲委派的核心类——ClassLoader

    在Java中,所有的类加载器都间接的继承自ClassLoader类,包括Ext、App类加载器(Bootstrap除外,因为它是C++实现的)

    // ClassLoader类 → loadClass()方法
    protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // 加锁
        synchronized (getClassLoadingLock(name)) {
            // 先尝试通过全限定名从自己的命名空间中查找该Class对象
            Class c = findLoadedClass(name);
            // 如果找到了则不需要加载了,如果==null,开始类加载
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 先将类加载任务委托自己的父类加载器完成
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果父类加载器为null,代表当前已经是ext加载器了
                        // 那么则将任务委托给Bootstrap加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 处理异常,抛出异常
                }
    
                if (c == null) {
                    // 如果都没有找到,则通过自定义实现的findClass
                    // 去查找并加载
                    long t1 = System.nanoTime();
                    c = findClass(name);
    
                    // 这是记录类加载相关数据的(比如耗时、类加载数量等)
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            // 是否需要在加载时进行解析,如果是则触发解析操作
            if (resolve) {
                resolveClass(c);
            }
            // 返回加载后生成的Class对象
            return c;
        }
    }
    
    // ClassLoader类 → findClass()方法
    protected Class findClass(String name) 
                throws ClassNotFoundException {
        // 直接抛出异常(这个方法是留给子类重写的)
        throw new ClassNotFoundException(name);
    }
    
    // ClassLoader类 → defineClass()方法
    protected final Class defineClass(String name, byte[] b,
            int off, int len) throws ClassFormatError
    {
        // 调用了defineClass方法,
        // 将字节数组b的内容转换为一个Java类
        return defineClass(name, b, off, len, null);
    }
    
    // ClassLoader类 → resolveClass()方法
    protected final void resolveClass(Class c) {
        // 调用本地(navite)方法,解析一个类
        resolveClass0(c);
    }
    
    // ClassLoader类 → getParent()方法
    @CallerSensitive
    public final ClassLoader getParent() {
        // 如果当前类加载器的父类加载器为空,则直接返回null
        if (parent == null)
            return null;
        // 如果不为空则先获取安全管理器
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            // 然后检查权限后返回当前classLoader的父类加载器
            checkClassLoaderPermission(parent,
                    Reflection.getCallerClass());
        }
        return parent;
    }
    

    ClassLoader类的关键方法,具体作用如下:

    loadClass(name,resolve):加载名称为name的类,加载后返回Class对象实例
    findClass(name):查找名称为name的类,返回是一个Class对象实例(该方法是留给子类重写覆盖的,在loadClass中,在父类加载器加载失败的情况下会调用该方法完成类加载,这样可以保证自定义的类加载器也符合双亲委托模式)
    defineClass(name,b,off,len):将字节流b转换为一个Class对象
    resolveClass(c):使用该方法可以对加载完生成的Class对象同时进行解析操作
    getParent():获取当前类加载器的父类加载器
    

    如何破坏双亲委派

    我们知道类的加载方式默认是双亲委派,如果我们有一个类想要通过自定义的类加载器来加载这个类,而不是通过系统默认的类加载器,说白了就是不走双亲委派那一套,而是走自定义的类加载器

    我们知道双亲委派的机制是ClassLoader中的loadClass方法实现的,打破双亲委派,其实就是重写这个方法,来用我们自己的方式来实现即可

    • 如果不想打破双亲委派模型,就重写ClassLoader类中的findClass()方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。
    • 而如果想打破双亲委派模型则需要重写ClassLoader类loadClass()方法(当然其中的坑也不会少)。典型的打破双亲委派模型的框架和中间件有tomcat

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论