Class 文件应当是字节流,可以通过文件、网络或内存等加载。
类加载过程
- 加载(Loading)
- 连接(Linking)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
加载、验证、准备、初始化和卸载这 5 个阶段的顺序是确定的,而解析阶段则不一定:
- Java 的动态绑定,解析阶段可以在初始化阶段之后
加载:
在加载阶段,虚拟机需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存(堆)中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
获取二进制字节流:
- 从 ZIP 包中读取
- 从网络中获取
- 运行时计算生成,常见于动态代理技术
- 由其他文件生成,典型场景是 JSP,由 JSP 文件生成对应的 Class 类
- 从数据库读取
使用类加载器来加载一个类,用户可以自定义类加载器(重写 loadClass() 方法),来控制字节流的获取方式。
对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建。
加载和连接阶段是交叉进行的,在加载的过程中包含连接阶段的动作,但两个阶段的开始时间的顺序是固定的。
连接 - 验证:
- 文件格式验证
- 只有通过这个验证后,字节流才会进入内存的方法区中进行存储
- 所以后面的验证阶段全部是基于方法区的存储结构进行的
- 元数据验证
- 字节码验证
- 符号引用验证
连接 - 准备:
为类变量分配内存并设置类变量初始值,在方法区中分配。
- 仅包括类变量(被 static 修饰的变量),不包括实例变量
- 初始值“通常情况”下是数据类型的零值。
- 符号引用:用一组符号来描述所引用的目标,符号可以实任何形式的字面量,引用的目标不一定要存在于内存中。符号引用的字面量形式明确定义在 Class 文件格式中。
- 直接引用:可以是直接指向目标的指针、相对偏移量或间接引用(句柄)
虚拟机实现可以根据需要选择:
- 在类被加载器加载时就对常量池中的符号引用进行解析
- 等到一个符号引用被使用前才去解析它
- 类或接口解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化:
初始化阶段才真正开始执行类中定义的字节码。 注意:类加载并不会立刻执行完全部的步骤,初始化步骤会在特定场景下触发
触发:
- 对一个类进行主动引用,会触发其的初始化
虚拟机规范规定,有且只有 5 种情况必须立即对类进行初始化:
- 遇到 new、getstatic、putstatic 或 invokestatic 这 4 条字节码指令时
- 使用 new 关键字实例化对象
- 读取或设置一个类的静态字段(被 final 修饰、或已在编译期把结果放入常量池的静态字段除外)
- 调用一个类的静态方法时
- 使用 java.lang.reflect 包的方法对类进行反射调用时
- 初始化当前类时,触发其父类的初始化
- 虚拟机启动时,指定的 main() 方法所在的那个类
- 动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_incokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化时
被动引用:
- TODO
初始化:
在准备阶段类变量已经赋过初始值,而在初始化阶段,才是根据程序员的代码计划去初始化类变量和其他资源。
初始化阶段是执行类构造器
() 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,顺序由语句在源文件中出现的顺序决定。静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,只能赋值,但不能访问。 public class Test {
static {
i = 0;
System.out.print(i); // 编译器报错:illegal forward reference
}
static int i = 1; // 按顺序,如果没有报错,最后 i 为 1
}
() 方法 与类的构造函数(() 方法)不同,它不需要显式地调用父类的构造器,虚拟机会保证在父类构造器在子类构造器之前执行。 - 因此虚拟机中第一个被执行的
() 方法的类肯定是 java.lang.Object
- 因此虚拟机中第一个被执行的
由于父类的
() 方法先执行,即父类的静态语句块要优先于子类的变量赋值操作 static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A; // Sub 被初始化后,B 的值为 2
}
如果一个类没有静态语句块,也没有对非 final 静态变量的赋值操作(只定义不赋值),那么编译器不会生成
() 方法 。- 接口中不能使用静态语句块,但会有静态变量赋值操作,所有也会生成
() 方法 - 但接口的
() 方法不需要先执行父接口的 () 方法,只有父接口的变量被使用时,父类才会初始化。 - 接口的实现类在初始化时,也不会执行接口的
() 方法
- 但接口的
虚拟机保证一个类的
() 方法只会被一个线程进入和执行,其他线程会阻塞等待。 - 所以如果一个类的
() 方法很耗时 ,那么可能会造成进程阻塞 特别是将方法的返回值赋值给静态变量作为初始值,以及耗时的静态代码块
public class Test1 {
static final int i = test();
// 当多个线程访问 Test1.i 时,只有一个线程进入,其他线程会被阻塞
private static int test() {
System.out.println(Thread.currentThread().getName() + " clinit");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
}
}
- 所以如果一个类的
注意区分:
():class init 类的构造器,静态变量的程序赋值和静态代码块 ():init 类的构造函数