Java 类的加载机制

Java 类的加载机制

概述

Java 中的每一个类或者接口【.java 】文件,在经过编译器编译之后,会生成对应的 .class 文件

类的加载机制指的是:将这些 .class 文件中大的二进制数据读入到内存中,并对数据进行校验,解析和初始化。最终,每一个类都会在方法区中保存一份他的元数据,在堆中创建一个与之对应的 class 对象。

类的生命周期

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

类的加载过程

加载、验证、准备、解析、初始化

类加载的时机

严格意义上来说,加载初始化,是类生命周期的两个阶段。

绝大多数情况下,遵循“什么时候初始化”来进行加载

当符合一下条件时【包括但不限于】,虚拟机内存中灭有找到对应类型信息,则必须对类进行 “初始化” 操作。:

  • 使用 new 实例化对象时、读取或者设置一个类的静态字段或方法时
  • 反射调用时,例如 Class.forName(“com.xxx.MyTest”)
  • 初始化一个类的子类,会首先初始化子类的父类
  • Java 虚拟机启动时标明的启动类
  • jdk8 之后,接口中存在 default 方法,这个接口的实现类初始化时,接口会进行初始化

【初始化阶段之前,必须经过:加载、验证、准备、解析

类的加载过程

类的加载过程包括 5 个阶段,其中【验证准备解析】被称为 “连接”阶段

在这里插入图片描述

这 5 个阶段,并不是严格意思上的按照顺序完成,在类的加载过程中,这些阶段互相混合,交叉运行,最终完成类的加载和初始化。

eg:

  • 在加载阶段,需要使用验证的能力去校验字节码的正确性。
  • 在解析阶段,需要使用验证的能力去校验符号引用的正确性。
  • 在加载阶段,生成 Class 对象时,需要解析阶段符号应用转直接引用的能力
  • ……

加载

加载时类加载过程的第一个阶段,在加载阶段,虚拟机需要完成一下三个事情:

  • 通过一个类的全限定名取找到其对应的 .class 文件
  • 将这个 .class 文件内的而精致数据读取出来,转化为方法区的运行时数据结构
  • 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为堆方法区中这些数据的访问入口

注意:

  • Java 虚拟机并没有规定类的字节流必须从 .class 文件中加载,在加载阶段,程序员可以通过自定义的类加载器,自定义读取的位置【网络、数据库等】

验证

.class 文件中的内容是字节码【可以由任何途径产出】,验证阶段的目的是保证文件内容的字节流符合 Java 虚拟机的规范,且这些内容信息运行后不会危害虚拟机自身的安全。

验证阶段会完成一下校验:

  • 文件格式验证:

    验证字节流是否符合 class 文件格式规范

    eg:

    ​ 是否以 0xCAFEBABE 开头

    ​ 主次版本号是否在当前虚拟机的处理范围内

    ​ 常量池中的常量是否有不被支持的类型

    ​ 省略号

  • 元数据验证:

    对字节码描述的元数据信息进行语义分析,要符合 Java 语言规范。

    eg:

    ​ 是否继承了不允许继承的类【被 final 修饰的类】

    ​ 类中的字段、方法是否和父类产生矛盾等

    ​ ……

  • 字节码验证:

    对类的方法进行校验分析,确保这些方法在运行室是合法的、符合逻辑等。

  • 符号引用验证:

    发生在解析阶段符号引用转为直接引用的时候。

    eg:

    ​ 确保符号引用的全限定名能找到对应的类

    ​ 符号引用中的类、字段、方法允许被当前类访问

    ​ ……

验证阶段非常重要,但不是必须的。 Java 虚拟机允许程序员主动取消这个阶段【缩短类的加载时间】。可以根据自身需求,使用 -Xverify:none 参数来关闭大部分的类验证措施。

准备

在这个阶段,类的静态字段信息【使用 static 修饰的变量】会得到内存分配,并且设置初始值。

  • 内存分配仅包括 static 修饰的变量,不包括实例变量,实例变量等到实例化对象时分配内存。
  • 初始值指的是:变量数据类型的默认值,而不是 Java 代码中的被显式赋予的值。但是,当字段信息被 final 修饰成常量时【ConstanrValue】时,这个初始值就是 Java 代码中显式赋予的值

eg:

public static int value = 3;
/**
 * 类变量 value 在准备阶段设置的初始值是 0,不是 3。将 value 赋值为 3 的 
 * putstatic 指令是在程序编译后,存放于类构造器 <clinit>() 方法中的
 * 所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
 * 当使用 final 修饰后:public static final int value = 3
 * 类变量 value 在准备阶段设置的初始值是 3,不是 0。
 *
 /
  • jdk8 取消永久代 (PermGen) 后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在 Java 堆中的。

解析

在这个阶段,虚拟机会把 class 文件中,常量池内的符号引用转换为直接引用。主要解析的是类或者接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。

可以将解析阶段中,符号引用转换为直接引用的过程理解为当前加载的这个类,和他所引用的类,正式进行 “连接” 的过程。

什么是符号引用

Java 代码在编译期间,并不知道最终引用的类型,具体指向内存中哪个位置,此时会使用一个符号引用,来表示具体引用的目标是谁,符号引用可以是任意值,只要能通过这个值定位到目标。

什么是直接引用

直接引用就是可以直接或间接指向目标内存位置的指针货句柄。

引用的类型,还未加载初始化怎么办?

现这种情况,会触发这个引用对应类型的加载和初始化。

初始化

类加载的最后一个步骤,初始化的过程,就是执行类构造器 () 方法的过程。

当初始化完成后,类中 static 修饰的变量会赋予程序员实际定义的 “值”,同时类中如果存在 static 代码块,也会执行这个静态代码块里面的代码。

() 方法的作用

在准备阶段,已经对类中的 static 变量赋予了初始值【这里赋予的烈性的默认值】,() 方法的作用是给这些变量赋予程序员实际定义的 “值”。同时类中如果存在 static 代码块,也会执行这个静态代码块里面的代码。

() 方法是什么

() 方法 和 方法是不同的,它们一个是“类构造器”,一个是实例构造器。

Java 虚拟机会保证子类 () 执行强,父类的 () 已经执行完毕。而 方法则需要显式的调用父类构造器。

() 方法有编译器自动生成,但不是必须生成的,只有这个类存在 static 修饰的变量或者或者类中存在静态代码块时,才会自动生成 () 方法。

加载过程总结

当一个符合 Java 虚拟机规范的字节流文件,经过**【加载、验证、准备、解析 、初始化】**这些阶段相互协作执行完成之后,加载阶段读取到的 class 字节流信息,会按照虚拟机规定的格式,在方法区保存一份,然后在 Java 堆中,创建一个 java.lang.Class 类对象,这个对象买哦输了这个类所有信息,也提供了这个类在方法区的访问入口。

方法区中,使用同一加载器时,每个类只会有一份 class 字节流信息

Java 堆中,使用同一加载器时,每个类只有一份 java.lang.Class 类的对象

类加载器

类加载器就是用来实现通过类的全限定名,获取类字节流数据。

三层类加载器介绍

启动类加载器(Bootstrap Class Loader):

负责加载 <JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数制定的路径,例如 jre/lib/rt.jar 里所有的 class 文件。由 C++ 实现,不是 ClassLoader 子类。

**拓展类加载器(Extension Class Loader):**负责加载 Java 平台中扩展功能的一些 jar 包,包括 <JAVA_HOME>\lib\ext 目录中 或 java.ext.dirs 指定目录下的 jar 包。由 Java 代码实现。

**应用程序类加载器(Application Class Loader):**用户开发的应用程序,就是由它进行加载的,负责加载 ClassPath 路径下所有jar包。

双亲委派模型

任何一个类加载器在接到一个类的加载请求时,都会让其父类进行加载,只有父类无法加载(没有父类)的情况下,才尝试自己加载。

在这里插入图片描述

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
{
    // 首先要保证线程安全
    synchronized (getClassLoadingLock(name)) {
        // 先判断这个类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                // 有父类,优先交给父类尝试加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父类加载失败,这里捕获异常,但不需要做任何处理
            }

            if (c == null) {
                // 没有父类,或者父类无法加载,尝试自己加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派模型的执行流程:

  1. 当加载一个类时,会先从应用程序类加载器的缓存里查找相应的类,如果能找到就返回对象,如果找不到就执行下面流程;
  2. 扩展加载器缓存中查找相应的类,如果能找到就返回对象,如果找不到就继续下面流程;
  3. 启动类加载器中查询相应的类,如果找到就返回对象,如果找不到就继续下面流程;
  4. 扩展加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就继续下面流程;
  5. 应用程序类加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就返回 ClassNotFound 异常。
  6. 如果某个类加载器加载了某个类,那么它会将该类的定义缓存起来,以便以后再次请求加载该类时可以直接返回缓存中的定义。

双亲委派模型好处

  • 避免类的重复加载

    当一个类加载器需要加载某个类时,会先委托给他的父类加载器去加载,如果福利加载器也找不到该类,才会自己尝试加载,避免重复加载一个类。

  • 确保类的安全性

    通过使用双亲委派模型,可以确保核心类库的安全性,同时也可以防止应用程序自己编写的类替换核心类库中的类,避免恶意代码的潜在危险。

    eg: jre 里面已经提供了 String 类在启动类加载时加载,那么用户自定义一个不安全的 String 类时,按照双亲委派模型就不会加载用户自定义的不安全的 String 类,这样就可以避免非安全问题的发生了。

缺点

  • 由于加载范围的限制,顶层的 ClassLoader 无法访问底层的 ClassLoader 所加载的类。

打破双亲委派模型方法

重写 loadClass() 方法打破双亲委派模型

原因:

类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值