目录
2.2 准备阶段:为 static 变量分配空间,设置默认值
从类的生命周期而言,一个类包括如下阶段:
类加载阶段分类:加载、链接(验证/准备/解析)、初始化(
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、初始化
初始化即调用
发行的时机
概括的说,类初始化是【懒惰的】,下面是会导致类初始化的情况
- 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点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!
评论记录:
回复评论: