美文网首页
类加载机制

类加载机制

作者: 面向对象架构 | 来源:发表于2022-12-22 00:31 被阅读0次

    一、类加载时机

    一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期将会经历 加载、验证、准备、解析、初始化、使用、卸载 七个阶段。

    其中验证、准备、解析三个部分统称为连接。

    六种情况必须立即对类进行“初始化”

    1. 遇到 newgetstaticputstaticinvokestatic 这四条字节码指令时,如果类型没有进行初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型 Java 代码场景有:
      • 使用 new 关键字实例化对象时
      • 读取或设置一个类型的静态字段(被 final 修饰、已在编译器把结果放入常量池的静态字段除外)时
      • 调用一个类型的静态方法时
    2. 使用 java.lang.reflect 包的方法对类型进行反射调用时,如果类型没有进行过初始化,则需要先触发其初始化。
    3. 当初始化类时,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
    4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
    5. 当使用 JDK 7 新加入的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStaticREF_putStatic
      REF_invokeStaticREF_newIncokeSpecial 四种类型的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
    6. 当一个接口中定义了 JDK 8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

    二、类加载过程

    2.1、加载

    “加载”阶段是整个“类加载”过程中的一个阶段。需要完成三件事情:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。

    加载阶段既可以使用 Java 虚拟机内置的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员通过自定义的类加载器去控制字节流的获取方式(重写一个类加载器的findClass()loadClass()方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性。

    数组类本身不通过类加载器创建,是由 Java 虚拟机直接在内存中动态构造出来的。

    加载阶段的典型应用

    • 从 ZIP 压缩包中读取,最终发展出 JAR、WAR格式。
    • 从网络中获取,典型的是 Web Applet。
    • 运行时计算生成,该场景使用最多的是动态代理技术。
    • 由其他文件生成,如 JSP生成对应的 Class 文件。
    • 从数据库中读取,比较少见,如 某些中间件服务器(SAP Netweaver)。
    • 从加密文件中获取,典型的防 Class 文件被反编译的保护措施,通过加载时解密Class 文件来保障程序运行逻辑不被窥探。

    2.2、验证

    确保 Class 文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。

    1. 文件格式验证
    2. 元数据验证
    3. 字节码验证
    4. 符号引用验证

    2.3、准备

    正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

    1. 进行内存分配的仅包括类变量,而不包括实例变量(实例变量随对象实例化时一起分配在Java堆中)。
    2. 这里的初始值“通常情况”下是数据类型的零值。
    public static int value = 123;
    // 准备阶段初始值为 0 而非 123,赋值123 在初始化阶段进行
    
    public static final int value = 123;
    // 存在 ConstantValue 属性,准备阶段会被初始化为 ConstantValue 属性所指定的初始值,这里即 123
    

    2.4、解析

    Java 虚拟机将常量池内的符号引用替换为直接引用的过程。

    具体还包括:

    • 类或接口的解析
    • 字段解析
    • 方法解析
    • 接口方法解析

    2.5、初始化

    直到初始化阶段,Java 虚拟机才真正开始执行类中编写的Java 程序代码,将主导权移交给应用程序。

    初始化阶段就是执行类构造器 <clinit>() 方法的过程。

    三、类加载器

    3.1、双亲委派模型

    加载阶段,需要用到类加载器来将 class 文件里面的内容搞到 JVM 中生成类对象。

    双亲委派模型用一句话讲就是子类加载器先让父类加载器去查找该类来加载,父类又继续请求它的父类直到最顶层,在父类加载器没有找到所请求的类的情况下,子类加载器才会尝试去加载,这样一层一层上去又下来。

    每个类加载器都有固定的查找类的路径,在JDK8的时候一共有三种类加载器。

    • 启动类加载器(Bootstrap ClassLoader),它是属于虚拟机自身的一部分,用 C++ 实现的,主要负责加载目录中或被 -Xbootclasspath 指定的路径中的并且文件名是被虚拟机识别的文件。它是所有类加载器的爸爸。
    • 扩展类加载器(Extension ClassLoader),它是 Java 实现的,独立于虚拟机,主要负责加载目录中或被 java.ext.dirs 系统变量所指定的路径的类库。
    • 应用程序类加载器(Application ClassLoader),它是Java实现的,独立于虚拟机。主要负责加载用户类路径(classPath)上的类库,如果我们没有实现自定义的类加载器那这玩意就是我们程序中的默认加载器。


      JVM_Parents-Delegation-Model

    为什么要提出双亲委派模型?
    其实就是为了让基础类得以正确地统一地加载。
    从上面的图可以看出,如果你也定义了一个 类,通过双亲委派模式是会把这个请求委托给启动类加载器,它扫描目录就找到了 jdk 定义的 类来加载,所以压根不会加载你写的 类,这就可以避免一些程序不小心或者有意的覆盖基础类。

    虽说是子类父类,但是加载器之间的关系不是继承,而是组合。

    public abstract class ClassLoader {
    
        private final ClassLoader parent;
        
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                // 先看看之前是否加载过
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        // 如果有父类就委托给父类
                        if (parent != null) {
                            c = parent.loadClass(name, false);
                        } else {
                            // 父类是空,说明是启动类加载器
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // ClassNotFoundException thrown if class not found
                        // from the non-null parent class loader
                    }
    
                    if (c == null) {
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
                        // 如果父类找不到就自己找
                        c = findClass(name);
    
                        // this is the defining class loader; record the stats
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    }
    
    

    在 JVM 中,类的唯一性是由类加载器实例和类的全限定名一同确定的,也就是说即使是同一个类文件加载的类,用不同的类加载器实例加载,在 JVM 看来这也是两个类。

    3.2、破坏双亲委派模型

    第一次破坏

    在 jdk 1.2 之前,那时候还没有双亲委派模型,不过已经有了 ClassLoader 这个抽象类,所以已经有人继承这个抽象类,重写 loadClass 方法来实现用户自定义类加载器。

    而在 1.2 的时候要引入双亲委派模型,为了向前兼容, loadClass 这个方法还得保留着使之得以重写,新搞了个 findClass 方法让用户去重写,并呼吁大家不要重写 loadClass 只要重写 findClass。

    这就是第一次对双亲委派模型的破坏,因为双亲委派的逻辑在 loadClass 上,但是又允许重写 loadClass,重写了之后就可以破坏委派逻辑了。

    第二次破坏

    第二次破坏指的是 JNDI、JDBC 之类的情况。

    首先得知道什么是 SPI(Service Provider Interface),它是面向拓展的,也就是说我定义了个规矩,就是 SPI ,具体如何实现由扩展者实现。

    像我们比较熟的 JDBC 就是如此。

    MySQL 有 MySQL 的 JDBC 实现,Oracle 有 Oracle 的 JDBC 实现,我 Java 不管你内部如何实现的,反正你们这些数据库厂商都得统一按我这个来,这样我们 Java 开发者才能容易的调用数据库操作,所以在 Java 核心包里面定义了这个 SPI。

    而核心包里面的类都是由启动类加载器去加载的,但它的手只能摸到或Xbootclasspath指定的路径中,其他的它鞭长莫及。

    而 JDBC 的实现类在我们用户定义的 classpath 中,只能由应用类加载器去加载,所以启动类加载器只能委托子类来加载数据库厂商们提供的具体实现,这就违反了自下而上的委托机制。

    具体解决办法是搞了个线程上下文类加载器,通过默认情况就是应用程序类加载器,然后利用获得类加载器来加载。

    这就是第二次破坏双亲委派模型。

    第三次破坏:热部署

    这次破坏是为了满足热部署的需求,不停机更新这对企业来说至关重要,毕竟停机是大事。

    OSGI 就是利用自定义的类加载器机制来完成模块化热部署,而它实现的类加载机制就没有完全遵循自下而上的委托,有很多平级之间的类加载器查找。

    第四次破坏:模块化

    在 JDK9 引入模块系统之后,类加载器的实现其实做了一波更新。

    当收到类加载请求,会先判断该类在具名模块中是否有定义,如果有定义就自己加载了,没的话再委派给父类。

    相关文章

      网友评论

          本文标题:类加载机制

          本文链接:https://www.haomeiwen.com/subject/intexdtx.html