目录
2.2 准备阶段:为 static 变量分配空间,设置默认值
从类的生命周期而言,一个类包括如下阶段:
类加载阶段分类:加载、链接(验证/准备/解析)、初始化(<cinit>()V方法 / 发生的时机),如下图所示:
1、加载
- 将类的字节码载入方法区中,内部采用 c++ 的 instanceKlass 描述 java 类,它的重要 field 域有:
- _java_mirror 即 java 的类镜像,起到桥梁作用,例如对 String 来说,就是 String.class, 作用是把 kclass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
_itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
- 加载和链接可能是交替运行的
注意:
- instanceKlass 这样的【元数据】是存储在方法区(1.8后的元空间内),但 _java_mirror是存储在堆中
- 可以通过HSDB工具查看
2、链接
链接又可以分为3个小步骤,如下:
2.1 验证阶段:验证类是否符合 JVM 规范,安全性检查
用 UE 等支持二进制的编辑器修改 HelloWorld.class 的魔数,在控制台运行
2.2 准备阶段:为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾(方法区),从 JDK 7 开始,存储于跟着类对象 _java_mirror 末尾(堆内存)
- static 变量:分配空间 和 赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变是是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
解析:将常量池中的符号引用解析为直接引用
// 类加载分析 - 演示 final 对静态变量的影响
public class T02_ClassLoad_Final_Static {
// 在准备阶段,仅仅只有分配空间,没有赋值
static int a;
// 在准备阶段,仅仅只有分配空间,赋值是在类的构造方法中
static int b = 10;
// 在准备阶段,20 属于ConstantValue,final+static 修饰的整型变量 在准备阶段就完成了赋值,编译器对其优化了
static final int c = 20;
// 在准备阶段,"hello" 也属于ConstantValue,static+final 修饰的字符串常量 在准备阶段就完成了赋值,编译器对其优化
static final String d = "hello";
// 在准备阶段,如果 static 变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
static final Object e = new Object();
}
上述代码,通过:javap -v T02_ClassLoad_Final_Static.class进行反编译,得到如下字节码。
2.3 解析:将常量池中的符号引用解析为直接引用。
将常量池中的符号引用解析为直接引用。符号引用仅仅只是符号,并不知道类、方法、属性到底在内存的哪个位置;而直接引用就可以确切知道类、方法、属性在内存的中位置。
代码示例如下:
// 解析的含义
public class T03_ClassLoad_Resolved {
public static void main(String[] args) throws ClassNotFoundException, IOException {
ClassLoader classLoader = T03_ClassLoad_Resolved.class.getClassLoader();
// 可以通过 HSDB工具进行查看:java -cp ./lib/sa-jdi.jar sun.jvm.hotspot.HSDB
// loadClass 方法不会导致类的解析和初始化: JVM_CONSTANT_UnresolvedClass com.jvm.t09_class_load.D
// D类没有被加载、解析
Class<?> c = classLoader.loadClass("com.jvm.t09_class_load.C");
// 下述会导致C类/D类的加载,JVM_CONSTANT_Class com.jvm.t09_class_load.D
// new C();
System.in.read();
}
}
// 类C
class C {
D d = new D();
}
// 类D
class D {
}
3、初始化
<cinit>()v 方法,初始化是类加载的最后一个阶段
初始化即调用 <cinit>()v,虚拟机会保证这个类的 【构造方法】的线程安全。
发行的时机
概括的说,类初始化是【懒惰的】,下面是会导致类初始化的情况
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时
- 子类初始化,如果父类还没初始化,会引发
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName
- new 会导致初始化
不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象 .class 不会触发初始化,因为 .class 在加载阶段就已经生成,所以不会被触发
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 initialize为 false 时
public class T04_ClassLoad_Init {
static {
// main 方法所在的类,总会被首先初始化
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 不会导致类初始化的情况
// 1、静态常量不会触发初始化
System.out.println(B.b);
// 2、类对象.class 不会触发初始化
System.out.println(B.class);
// 3、创建该类的数组不会触发初始化
System.out.println(new B[0]);
// 4、不会初始化类 B,但会加载 B、A
ClassLoader c1 = Thread.currentThread().getContextClassLoader();
c1.loadClass("com.jvm.t09_class_load.B");
// 5、不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.jvm.t09_class_load.B", false, c2);
// 导致类初始化的情况
// 1、首次访问这个类的静态变量或静态方法时
System.out.println(A.a);
// 2、子类初始化,如果父类还没有初始化,会引发
System.out.println(B.c);
// 3、子类访问父类静态变量,只触发父类初始化
System.out.println(B.a);
// 4、会初始化类 B,并先初始化类 A
Class.forName("com.jvm.t09_class_load.B");
}
}
class A {
static int a = 0;
static {
System.out.println("a init");
}
}
class B extends A {
final static double b = 5.0;
static boolean c = false;
static {
System.out.println("b init");
}
}
4、类加载常见问题-练习
从字节码分析,使用a, b, c 这三个常量是否导致致 E 初始化
// 类加载分析 - 练习
public class T05_ClassLoad_Practice {
public static void main(String[] args) {
System.out.println(E.a);
System.out.println(E.b);
System.out.println(E.c);
}
}
class E {
// a, b 是静态常量,一个是整型,一个字符串常量。是在类链接的准备阶段就已经完成了赋值
public static final int a = 10;
public static final String b = "hello";
// c 是包装类型,20进行装箱,c 是在初始化阶段完成赋值
public static final Integer c = 20; // Integer.valueOf(20)
static {
System.out.println("init E");
}
}
5、类加载常见问题-练习2
典型应用 - 完成懒惰初始化单例模式
// 类加载分析 - 练习2
public class T05_ClassLoad_PracticeV2 {
public static void main(String[] args) {
//Singleton.test(); // 调用静态方法test,不会触发类的初始化
Singleton.getInstance(); //
}
}
class Singleton {
public static void test() {
System.out.println("test");
}
private Singleton() {}
// 静态内部类,好处就是可以访问外部类的资源:构造方法,虽然构造方法是私有的,但在静态内部类还是可以访问的
// 内部类中保证单例
private static class LazyHolder {
private static final Singleton SINGLETON = new Singleton();
static {
System.out.println("lazy holder init");
}
}
// 第一次调用,getInstance 方法,才会导致内部类加载和初始化其静态成员
public static Singleton getInstance() {
return LazyHolder.SINGLETON;
}
}
以上实现的特点是:
- 懒惰实例化
- 初始化时的线程安全是有保障的
文章最后,给大家推荐一些受欢迎的技术博客链接:
- Hadoop相关技术博客链接
- Spark 核心技术链接
- JAVA相关的深度技术博客链接
- 超全干货--Flink思维导图,花了3周左右编写、校对
- 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
- 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
- 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!