Java类加载机制

1 类生命周期

一个类的完整生命周期如下图所示:

其中类加载过程包括5个阶段:加载、验证、准备、解析、初始化,其中除了解析外,其余4个阶段的顺序是固定的。 它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定)

另外需要注意的一点是,这里说的“顺序固定”是指开始顺序,而不是按顺序进行或完成, 因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。

1.1 加载

在加载阶段需要完成3件事情:

  1. 通过类全限定名获取此类的二进制字节流

    对于从哪里获取class文件,没有明确规定,主要有下列几种方式:

    • 本地系统直接加载
    • 通过网络下载
    • 从zip、jar等归档文件中加载
    • 从数据库中读取
    • 将Java元文件动态编译为class文件
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

  3. 在Java堆中生成一个该类的对象,作为对方法区中这些数据的访问入口。

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

1.2 验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。验证阶段大致会完成4个阶段的检验动作:

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

1.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  1. 这时候进行内存分配的仅包括类变量( Class Variables ,即静态变量,被 static 关键字修饰的变量,只与类相关,因此被称为类变量),而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在 Java 堆中。

  2. 这里所设置的初始值通常都是数据类型的默认零值,而不是在java代码中被显示赋予的值。

1.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符 7 类符号引用进行。

注: 直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,使用符号引用时,被引用的目标不一定已经加载到内存中。

1.5 初始化

  • 目的

    初始化阶段是执行初始化方法的过程,是类加载的最后一步,主要对类变量进行初始化。这一步 JVM 才开始真正执行类中定义的 Java 程序代码(字节码)。

  • 步骤

    1. 如果该类没有被加载和连接,则先加载和连接该类;
    2. 如果该类的直接父类还没有被初始化,则先初始化其直接父类 ;
    3. 如果类中有初始化语句,则系统依次执行这些初始化语句。
  • 时机

    只有当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:

    1. 当遇到 newgetstaticputstaticinvokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname("..."), newInstance() 等等。如果类没初始化,需要触发其初始化。
    3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
    4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
    5. 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

1.6 卸载

Java虚拟机将结束生命周期的几种情况

  • 执行了System.exit()方法;
  • 程序正常执行结束;
  • 程序在执行过程中遇到了异常或错误而异常终止;
  • 由于操作系统出现错误而导致Java虚拟机进程终止。

2 类加载器

2.1 类加载器层次

类加载器可以大致划分为以下三类:

  1. 启动类加载器

Bootstrap ClassLoader,负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。

  1. 扩展类加载器

    Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  2. 应用程序类加载器

    Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责**加载用户类路径(ClassPath)**所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

2.2 类加载方式

类加载有三种方式:

  1. 命令行启动应用时候由JVM初始化加载;
  2. 通过Class.forName()方法动态加载 ;
  3. 通过ClassLoader.loadClass()方法动态加载。

2.3 双亲委派机制

系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。即在类加载的时候:

  1. 系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。
  2. 加载的时候,首先会把该请求委派给父类加载器的 loadClass() 处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader 中。
  3. 当父类加载器无法处理时,才由自己来处理。

使用双亲委派机制的优势在于

  1. Java 的核心 API 不被篡改,保证了 Java 程序的稳定运行

    比如我们编写一个称为 java.lang.Object 类的话,那么程序运行的时候,系统就会出现多个不同的 Object 类。

  2. 可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类), 防止内存中出现多份同样的字节码 。

2.4 自定义类加载器

除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader。 需要注意的是:

  1. 最好不要重写loadClass方法,因为这样容易破坏双亲委托模式;
  2. 调用loadClass函数时,需要使用全限定名称,如com.aaa.bbb.Test。

参考资料

  1. https://pdai.tech/md/java/jvm/java-jvm-classload.html
  2. https://javaguide.cn/java/jvm/classloader
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信