Fork me on GitHub

类加载机制笔记

1. 六种主动使用类的场景

按照 JVM 规范,每个类或者接口被 Java 程序首次主动使用时,才会对其进行初始化。

以下六种操作会导致类的初始化,除了这六种,其他情况都属于被动使用,不会导致类的加载和初始化。

  1. 通过 new 关键词。
  2. 访问类的静态变量(但 final 修饰的静态变量实质上是静态常量,不属于此范畴,不会初始化类)。
  3. 访问类的静态方法。
  4. 对类进行反射操作。
  5. 初始化子类会导致父类的初始化。
  6. 启动类。也就是 main 函数所在的类会被初始化。

2. 类加载过程

分成三个阶段:加载阶段、连接阶段、初始化阶段。

2.1 加载阶段

① 查找并加载类的二进制数据文件(即 class 文件)读取到内存中;② 然后将字节流代表的静态存储结构转换成方法区中运行时的数据结构;③ 并且在堆内存中生成一个该类的 Class 对象,作为访问方法区数据结构的入口。

2.2 连接阶段

细分为三个阶段:验证、准备、解析。

2.2.1 验证

概述:确保类文件的正确性,比如 class 的版本、class 文件的魔术因子是否正确。

主要验证:

  1. 文件格式验证。确定文件类型、JDK 版本号、文件完整性、变量类型是否被支持、引用指向是否正确等。
  2. 元数据验证。对 class 字节流进行语义分析,确保 class 字节流符合 JVM 规范的要求。
    • 比如检查类的父类、是否继承了 final 类,是否为抽象类,方法重载的合法性等。
  3. 字节码验证。主要验证程序的控制流程,比如循环、分支等。
    • 保证线程在程序计数器中的指令不会跳转到不合法的字节码指令中去。
    • 保证类型转换合法。
    • 保证虚拟机栈中的操作栈类型和指令代码能够正确执行。
  4. 符号引用验证。验证符号引用转换成直接引用时的合法性,保证解析动作的顺利执行。

2.2.2 准备

为类的静态变量分配内存,并且为其初始化默认值(而非程序编写时的赋值)。

类变量的内存会被分配到方法区中,实例变量会被分配到堆内存中。

1
2
3
4
5
6
public class LinkedPrepare{
private static int a = 10; // 准备阶段,a 是 0(Int 的 default 值)
private final static int b = 10;
// 准备阶段,b 就是 10,因为 b 是静态常量,本就没有连接阶段
// ,在编译阶段就被赋值 10 了。
}

2.2.3 解析

概述:把类中的符号引用转换为直接引用。

  • 比如一些成员对象某 object,可以访问它的可见的属性和方法,但在 class 字节码中不是这样。
  • 在 class 字节码中,它被编译成符号引用。在类解析时,需要转换成直接引用,才能正确地找到对应堆内存中的该 object 数据结构。

解析过程主要针对类接口、字段、类方法和接口方法这四类进行。

  1. 类接口:如果是 object ,需要对这个 object 类先进行加载,会经历 classLoader 全过程。
  2. 字段的解析:根据继承关系自下而上地查找,找到了就可以返回字段的引用。
  3. 类方法的解析:根据继承关系自下而上地查找,如果找到了方法描述和目标方法完全一致的方法(不能是接口方法),则返回这个方法的引用。
  4. 接口方法的解析:根据继承关系自下而上地查找,如果找到了方法描述和目标方法完全一致的方法(不能是类方法),则返回这个方法的引用。

2.3 初始化阶段

概述:为类的静态变量赋予正确的初始值。

Tips:

  1. 赋值的是<clinit>()方法,它是在编译阶段生成的,已经包含在 class 文件中了,而且能够保证顺序性。
  2. 父类的静态变量总是能够得到优先赋值。
  3. 如果某个类既没有静态代码块,也没有静态变量,那么它就没有生成<clinit>()方法的必要了。

Ps:Clinit方法,推荐阅读:类加载过程<clinit>()部分,提炼如下:

  1. 由编译器自动收集类中的所有类变量的赋值动作静态语句块中的语句合并产生的。
  2. 虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。
  3. <clinit>()方法可以保证百分之百同步,因为如果有多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待。

3. JVM 的类加载器

有三种:BootStrap ClassLoader、Ext ClassLoader、Application ClassLoader。

1

  1. BootStrap ClassLoader.通过-Xbootclasspath指定被 JVM 认可的类库路径
  2. Ext ClassLoader.通过java.ext.dirs加载指定路径重的类库
  3. Application ClassLoader.加载classpath上的类库

3.1 自定义类加载器

需要继承 ClassLoader 或其子类。要覆写findClass()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protected Class<?> findClass(String name) throws ClassNotFoundException{
// 读取 class 的二进制数据
byte[] classBytes = this.readClassBytes(name);
if(null == classBytes || classBytes.length == 0){
throw new ClassNotFoundException("Can not load the class"+name);
}

// classLoader.defineClass()方法,将字节数组转化成 class 的 instance
// 参数:要定义的类的名称,二进制字节数组,数组偏移量,偏移量开始多长的 byte
return this.defineClass(name,classBytes,0,classBytes.length);
}

// 将 class 文件读入内存
private byte[] readClassBytes(String name) throws ClassNotFoundException{
String classPath = name.replace(".","/");
Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
if(!classFullPath.toFile().exists())
throw new ClassNotFoundException("The class " + name + " not found.");
// 字节数组输出流
try(ByteArrayOutPutStream baos = new ByteArrayOutputStream()){
Files.copy(classFullPath,baos);
return baos.toByteArray();
}catch(IOException e){
throw new ClassNotFoundException("load the class " + name + " occur error.",e);
}
}

3.1.1 跳过双亲委托机制的两种方法

应用在自定义类加载器上:

  1. 将扩展类加载器作为 MyClassLoader(自定义)的父加载器。
    • 会跳过 Application ClassLoader。
  2. 在构造 MyClassLoader 的时候,指定其父类加载器为 null。

3.1.2 破坏双亲委托机制的方法

应用在自定义类加载器上:

  • 在自定义的类加载上(或其子类),重写loadClass()方法。
  • 在该方法中:
    1. 先对类的全路径名称加锁,保证仅被加载一次,线程安全;
    2. 查看缓存中是否有该类,若无进入下一步;
    3. 判断类的全路径是否以 java 和 javax 开头,若是,则委托给系统类加载器;
    4. 若不是,可尝试以自定义的类加载器加载。
    5. 若尝试失败,可继续委托给其父类加载器或者系统加载器进行加载。

3.1.3 类加载器的命名空间

  • 每一个类加载器都有各自的命名空间,其中的每一个 class 都是独一无二的。
  • 但是使用不同的类加载器,或者同一个类加载器的不同实例,去加载同一个 class ,会在堆内存和方法区产生多个 class 对象。

【准确说法】同一个 class 实例在同一个类加载器命名空间之下是唯一的。

3.1.4 运行时包

  • 编写代码时,包名和类名构成类的全限定名称。
  • 在 JVM 运行时,class 会有一个运行时包,由类加载器的命名空间和类的全限定名称共同组成。比如:
1
BootstrapClassLoader.ExtClassLoader.AppClassLoader.MyClassLoader.top.likehui.service.test

3.1.5 初始类加载器

  • 如果某个类 C 被类加载器 CL 加载,那么 CL 就被称为 C 的初始类加载器。
  • JVM 为每个类加载器维护了一个列表,记录了将该类加载器作为初始加载器的所有 class。
  • 在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,都会标识为该类的初始类加载器。

3.1.6 类的卸载

满足以下三个条件时,一个 Class 会被 GC 回收,即被卸载。

  1. 该类所有的实例都已经被 GC;
  2. 加载该类的 ClassLoader 实例被回收;
  3. 该类的 class 实例没有在别的地方被引用。

2

【重要】此时方法区中关于这个废弃类的信息等数据也会被卸载(应该是有目的性的那种,而不是范围性的 GC,两者有较大区别)

-------------The End-------------