05 Java类加载
2021-09-22 本文已影响0人
攻城老狮
1 类的生命周期
类加载阶段分为:加载,链接(验证,准备,解析),初始化(<init>()V方法)。

2 加载
- 将类的字节码载入方法区中,内部采用 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是存储在堆中

3 链接
3.1 验证阶段:验证类是否符合 JVM 规范,安全性检查
用支持二进制的编辑器修改 class 文件的魔数cafebabe,在控制台运行。会报错,说明验证class文件失败。

3.2 准备阶段:为 static 变量分配空间,设置默认值
- static 变量在 JDK 7 之前存储于 instanceKlass 末尾(方法区),从 JDK 7 开始,存储于跟着类对象 _java_mirror 末尾(堆内存)
- static 变量:分配空间 和 赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
- 如果 static 变量是 final 的基本类型或者字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
- 如果 static 变是是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
- Java代码
// 类加载分析 - 演示 final 对静态变量的影响
public class TestDemo {
// 在准备阶段,仅仅只有分配空间,没有赋值
static int a;
// 在准备阶段,仅仅只有分配空间,赋值是在类的构造方法中(初始化阶段 <cinit>V())
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();
}
- 反编译Java代码
{
static int a; // 在准备阶段,仅仅只有分配空间,没有赋值
descriptor: I
flags: ACC_STATIC
static int b; // 在准备阶段,仅仅只有分配空间,赋值是在类的构造方法中(初始化阶段 <cinit>V())
descriptor: I
flags: ACC_STATIC
static final int c; // 在准备阶段,20 属于ConstantValue,final+static 修饰的整型变量 在准备阶段就完成了赋值,编译器对其优化了
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 20
static final java.lang.String d; // 在准备阶段,"hello" 也属于ConstantValue,static+final 修饰的字符串常量 在准备阶段就完成了赋值,编译器对其优化
descriptor: Ljava/lang/String;
flags: ACC_STATIC, ACC_FINAL
ConstantValue: String hello
static final java.lang.Object e; // 在准备阶段,如果 static 变量是final的,但属于引用类型,那么赋值也会在初始化阶段完成
descriptor: Ljava/lang/Object;
flags: ACC_STATIC, ACC_FINAL
public com.yqj.TestDemo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 5: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/yqj/TestDemo;
static {}; //初始化阶段
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: bipush 10
2: putstatic #2 // Field b:I
5: new #3 // class java/lang/Object
8: dup
9: invokespecial #1 // Method java/lang/Object."<init>":()V
12: putstatic #4 // Field e:Ljava/lang/Object;
15: return
LineNumberTable:
line 9: 0
line 15: 5
}
3.3 解析:将常量池中的符号引用解析为直接引用
将常量池中的符号引用解析为直接引用。符号引用仅仅只是符号,并不知道类、方法、属性到底在内存的哪个位置;而直接引用就可以确切知道类、方法、属性在内存的中位置。
4 初始化
<cinit>()v 方法,初始化是类加载的最后一个阶段。初始化即调用 <cinit>()v,虚拟机会保证这个类的 【构造方法】的线程安全。类初始化是【懒惰的】。
- 类的初始化时机
- main 方法所在的类,总会被首先初始化
- 首次访问这个类的静态变量或静态方法时(final static的基本类型和字符串除外)
- 子类初始化,如果父类还没初始化会引发初始化
- 子类访问父类的静态变量,只会触发父类的初始化
- Class.forName()
- new 会导致初始化
- 不会导致类初始化的情况
- 访问类的 static final 静态常量(基本类型和字符串)不会触发初始化
- 类对象 .class 不会触发初始化,因为 .class 在加载阶段就已经生成,所以不会被触发
- 创建该类的数组不会触发初始化
- 类加载器的 loadClass 方法
- Class.forName 的参数 2 initialize为 false 时
- 测试类初始化的代码实例
public class TestDemo {
static {
// main 方法所在的类,总会被首先初始化
System.out.println("main init");
}
public static void main(String[] args) throws ClassNotFoundException {
// 不会导致类初始化的情况
// 1、final static的基本类型静态常量不会触发初始化
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.yqj.B");
// 5、不会初始化类 B,但会加载 B、A
ClassLoader c2 = Thread.currentThread().getContextClassLoader();
Class.forName("com.yqj.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.yqj.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");
}
}