关于类的加载

类的加载过程

当我们 new 一个对象,或者使用对象的静态方法,静态变量等操作时,JVM 会先检查类是否被加载(我们知道,它被加载到方法区),如果未加载,则使用类加载器(ClassLoader)将类进行加载并初始化。这里需要注意的是,初始化并非是一定发生的。调用类的静态常量(静态常量存在方法区中,在链接阶段被初始化,静态变量也存在于方法区中,不过要在初始化阶段通过<clinit>()方法初始化)等操作并不会触发类的初始化。

类的生命周期可以分为 7 步——加载验证准备解析初始化使用卸载。其中,类的初始化过程为前五步。

验证,准备,解析过程可以归结为链接过程。因而,一般说类的初始化过程分为三步——加载链接初始化

再次强调,类的初始化不是一定发生的。但是如果要使用一个类,其加载和链接是必定发生的。

加载

加载主要完成——

  1. 通过全类名获取定义类的二进制字节流(通过 ZIP,JAR 包,Class 文件以及其他)到内存中
  2. 将字节流代表的静态存储结构(也就是 Class 字节码)转换为方法区的运行时数据结构
  3. (!)中生成一个代表该类的java.lang.Class对象,它是方法区的这些数据的访问入口

总之,是把 Class 字节码加载到方法区中,然后在堆中生成Class对象作为其入口。

其中,第一步(获取类的二进制字节流)是高度可自定义的。可以自定义类加载器(重写类加载器的loadClass()方法)来控制字节流的获取方式。

数组类型不通过类加载器执行,而是 JVM 直接创建。

关于这一步,类加载器双亲委派机制是需要学习的。

加载阶段和链接阶段是可以并行执行的。

验证

验证是链接的第一步,它主要是验证 Class 文件(当然,已经被加载到方法区了)的合法性。

准备

准备阶段主要为 static 变量在方法区(!)中分配内存并设置初始值

该阶段,静态变量和静态常量的行为是不同的,比如我们定义public static int variable = 123; public static final int constant = 456;,在这一步中,variable会被赋int型的零值(0),而constant会被赋值 456。这表明——

  • 静态常量的初始化在准备(链接)阶段就被完成。
  • 静态变量的真正的初始化在类初始化过程<clinit>()方法中进行。在准备阶段只会被赋变量类型的初始值。

因此,调用类的静态常量不会触发类的初始化操作,就是因为其在准备阶段已经被初始化过了,可以直接使用。

解析

解析就是将常量池中的符号引用替换为直接引用的过程。解析动作主要针对类或接口,字段(field),类方法,接口方法,方法类型,方法句柄和调用限定符这 7 类符号引用进行。

这里的直接引用是直接指向目标的指针,相对偏移量或一个间接定位到目标的句柄。在运行时,这些符号引用必须被转换成直接引用,因此 JVM 才能明确获得它们的位置。比如,当程序要执行一个方法,系统就需要明确知道这个方法所在的位置。JVM 为每个类都准备了一个方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只需要知道它在方法表中的偏移量。(有点麻,这个之后肯定还得继续学习)

总之,解析是虚拟机将常量池中的符号应用替换为直接引用的过程,也就是得到类或字段,方法在内存中的指针或偏移量

初始化

初始化是类加载的最后一步,其真正地执行类中定义的 Java 程序代码(static 声明和 static 初始化块)。它会合并所有 static 的代码,将其组合为<clinit>()方法并执行。类的初始化并非是一定会被执行的。

类的初始化只有在主动使用类(静态变量,静态方法,实例变量,实例方法……)时出现,其有五种情况——

  1. 当遇到 new 、 getstatic、putstatic 或 invokestatic 这 4 条直接码指令时,比如 new 一个类,读取一个静态字段(未被 final 修饰)、或调用一个类的静态方法时。
    • 当 jvm 执行 new 指令时会初始化类。即当程序创建一个类的实例对象。
    • 当 jvm 执行 getstatic 指令时会初始化类。即程序访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)。
    • 当 jvm 执行 putstatic 指令时会初始化类。即程序给类的静态变量赋值。
    • 当 jvm 执行 invokestatic 指令时会初始化类。即程序调用类的静态方法。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时如 Class.forname(“…”).newInstance() 等等。 如果类没初始化,需要触发其初始化。
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化。
  4. 当虚拟机启动时,用户需要定义一个要执行的主类 (包含 main 方法的那个类),虚拟机会先初始化这个类。
  5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类。
  6. 「补充 issue745 当一个接口中定义了 JDK8 新加入的默认方法(被 default 关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

卸载

类的卸载就是该类的Class对象被 GC。

类的卸载有三个前提——

  1. 该类所有实例对象被 GC,即堆中不存在该类的实例对象
  2. 该类没有在其它任何地方被引用
  3. 该类的类加载器实例被 GC

JDK 自带的三个类加载器 BootstrapClassLoader,ExtClassLoader,AppClassLoader 不会被 GC,因而它们加载的类不会被卸载

学这些只是让我更加清楚地认识到,JVM 和字节码是必须要深入学习的…

类加载器(ClassLoader)

类加载器的作用就是将。class 文件加载到内存的方法区中。

Java 内置了三个 ClassLoader。除了 BootstrapClassLoader,其他类加载器都由 Java 实现,它们都继承自 java.lang.ClassLoader。按加载的优先级,它们由顶层向底层分为:

  1. BootstrapClassLoader是启动类加载器,它由 C++实现,加载%JAVA_HOME%/lib目录下的 jar 包和类,或者被-Xbootclasspath参数指定的路径中的所有类。
  2. ExtensionClassLoader是扩展类加载器,它加载目录%JRE_HOME%/lib/ext下的 jar 包和类,或者被java.ext.dirs系统变量所指定路径下的 jar 包。
  3. AppClassLoader是应用程序类加载器,它面向用户,加载当前应用 classpath 下所有 jar 包和类。

双亲委派模型

每一个类都有加载它的类加载器。类加载器默认使用双亲委派模型。即类加载时,系统(该类加载器)首先判断当前类是否被加载过,如果已加载则直接返回,如果未加载,则尝试加载。

加载时,该类加载器首先会把请求委派给父类加载器的loadClass()处理。因此,该请求会被递归传送到顶层的启动类加载器BootstrapClassLoader中。当父类加载器无法处理时,才由自己处理。当父类加载器为 null 时,会启动类加载器BootstrapClassLoader作为父类加载器。

双亲委派模型可以避免类的重复加载,并保证 Java 的核心 API 不被篡改

应该注意,这里的“双亲”只是用来指代“辈分”,而不是用来指代“父母”。

自定义类加载器

通过继承ClassLoader,我们可以自定义自己的类加载器。