类加载Class Loading
JVM 何时、如何把 Class 文件加载到内存,形成可以直接使用的 Java 类型,并开始执行代码?
类的生命周期
加载 - 连接(验证、准备、解析)- 初始化 - 使用 - 卸载。
注意,加载、验证、准备、初始化顺序是确定的,但是不是按部就班地「执行」,而是按部就班地「开始」。
另外,为了支持 Java 语言的运行时动态绑定,解析阶段有时候可以在初始化阶段之后再开始。
什么时候进行类加载?
JVM 规范中没有规定什么时候加载类,但是对于初始化时机有严格规定(而加载、验证、准备必须要在初始化之前开始):
触发初始化的条件
- 遇到 new、getstatic、putstatic、invokestatic 指令。(final static 字段除外,它是准备阶段被直接赋值)
- 反射调用。
- 初始化时如果父类没有初始化过,会先进行父类的初始化。(接口除外)(对静态字段,只有直接定义这个字段的类,才会被初始化。看下面代码)
- 虚拟机启动时,会优先初始化用户指定的主类。
- 初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。
- 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化。
public static void main(String[] args) {
System.out.println(B.a);
/*
* 输出:
* A
* 123
*/
}
static class A {
static int a = 123;
static {
System.out.println("A");
}
}
static class B extends A {
static {
System.out.println("B");
}
}
类加载的过程
包括加载、连接(验证、准备、解析)、初始化。
加载 Loading
- 通过类的全限定名来获取该类的二进制流。
- 把二进制流转为方法区的运行时数据结构。
- 内存中生成 Class 对象。
开发人员也可以通过自定义的类加载器来控制字节流的获取方式(即重写类加载器的 loadClass
方法)。
连接 Linking
验证 Verification
验证字节信息是否符合要求。
准备 Preparation
对类变量分配内存,并设置初始值(指基本数据类型的零值)。
如果是 final static 常量,则直接赋值。(JVM 如果发现 Class 文件常量池里,类字段的字段属性表中存在 ConstantValue 属性,会在准备阶段设置为 ConstantValue 属性所指定的值。比如编译期 final static 类常量值会放在 ConstantValue 里。)
解析 Resolution
把常量池里的一些符号引用替换为直接引用(内存地址)。
比如 invokestatic、invokespecial、final 方法,编译期可知,且运行期不可变的方法。
初始化 Initialization
执行类的 <clinit>
。
<clinit>
方法在编译期根据类变量赋值语句、静态语句块来生成的(顺序跟源代码里定义的顺序相同)。如果源代码没有这些语句,就不会生成该方法。
JVM 会通过加锁保证类的 <clinit>
方法只会执行一次。即在多线程环境中,只能有一个线程去执行,其他线程都需要阻塞等待,直到执行完成。