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代码
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代码
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 时
- 测试类初始化的代码实例
java">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");
}
}
相关文章
06 Java类加载器
1 类加载器概念 1.1 加载概念 加载指的是将类的 class 文件读入到内存,并为之创建一个 java.lang.Class 对象,也就是说,当程序中使用任何类时,系统都会为之建立一个 java.lang.Class 对象。类的加载由类加载器完成,类加…
基于SpringBoot的Dubbo启动
dubbo框架 image.png1 模块添加 新建三个模块,分别表示消息的提供者,消息的消费者和公共模块 image-20210923104204300.pngdubbo-parent的pom 说明:加入springboot的父依赖,作为版本控制 <parent><groupId>org.spring…
01 NIO 文件编程
一 ByteBuffer 1 API使用案例 1.1 使用方式 向 buffer 写入数据,例如调用 channel.read(buffer)调用 flip() 切换至读模式 flip会使得buffer中的limit变为position,position变为0 从 buffer 读取数据,例如调用 buffer.get()调用 clear() 或者…
02 NIO网络编程
1 网络编程 1 阻塞 1.1 阻塞模式概述 阻塞模式下,相关方法都会导致线程暂停 ServerSocketChannel.accept 会在没有连接建立时让线程暂停SocketChannel.read 会在通道中没有数据可读时让线程暂停阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu&#…
SpringBoot整合日志框架
1 Slf4j门面技术 SpringBoot采用日志门面技术,使得面向开发者的日志记录API是相同的,但是实现层还是由每个框架来决定的。SpringBoot帮我们已经做好了。它的日志门面选用的就是 SLF4J,而日志实现选用的是Logback。 如果log4j和JUL日志框架想采…
Ubuntu20.04 arm64 MySQL8 安装
项目需要搭建环境,用于测试mysql的主从复制原理。在网上搜索很久后,发现并没有对 mysql8 在 ubuntu20.04LTS arm64 下的详细搭建和配置说明,故本文整理目前网上的一些配置信息,全流程搭建起mysql8环境。 包括:mysql的安…