【JVM系列笔记】类生命周期

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB MySQL 版,Serverless 5000PCU 100GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 类的生命周期包括加载、连接(验证、准备、解析)、初始化、使用和卸载五个阶段。加载时,类加载器根据全限定名获取字节码,然后在方法区中创建InstanceKlass对象,并在堆上生成对应的Class对象。连接阶段验证字节码的正确性,准备阶段为静态变量分配内存并赋初始值,解析阶段将符号引用转换为直接引用。初始化阶段执行clinit方法,如静态变量赋值和静态代码块。类的初始化在访问静态成员、使用Class.forName、创建类实例或其子类时触发。

类的生命周期描述了一个类加载、使用、卸载的整个过程。整体可以分为:加载、连接、初始化、使用、卸载。其中连接又分为验证、准备、解析三个子阶段。

1. 加载

加载(Loading)阶段分三步

  1. 类加载器根据类的全限定名通过不同的渠道以二进制流的方式获取字节码信息,程序员可以使用Java代码拓展的不同的渠道,如本地磁盘获取,CGLIB动态代理生成,Applet技术网络获取。
  2. 类加载器在加载完类之后,Java虚拟机会将字节码中的信息保存到方法区中,方法区中生成一个InstanceKlass对象,保存类的所有信息,里边还包含实现特定功能比如多态的信息。
  3. Java虚拟机同时会在堆上生成与方法区中数据类似的java.lang.Class对象,作用是在Java代码中去获取类的信息以及存储静态字段的数据(JDK8及之后)。

为什么不直接从方法区加载信息,节约资源呢?

对于开发者来说,只需要访问堆中的Class对象而不需要访问方法区中所有信息。

这样Java虚拟机就能很好地控制开发者访问数据的范围。

2. 连接

连接阶段分为三个子阶段:

  • 验证,验证内容是否满足《Java虚拟机规范》。
  • 准备,给静态变量赋初值。
  • 解析,将常量池中的符号引用替换成指向内存的直接引用。

2.1. 验证

验证的主要目的是检测Java字节码文件是否遵守了《Java虚拟机规范》中的约束。这个阶段一般不需要程序员参与。主要包含如下四部分。

  1. 文件格式验证,文件是否以0xCAFEBABE开头,主次版本号是否满足当前Java虚拟机版本要求。
  2. 元信息验证,例如类必须有父类(super不能为空)。
  3. 验证程序执行指令的语义,比如方法内的指令执行中跳转到不正确的位置。
  4. 符号引用验证,例如是否访问了其他类中private的方法等。

JDK1版本号为45,JDK8版本号为52;

主版本号不能高于运行环境主版本号,如果主版本号相等,副版本号也不能超过。

2.2. 准备

准备阶段为静态变量(static)分配内存并设置初值。

只讨论JDK8及之后的版本

数据类型

初始值

int

0

long

0L

short

0

char

'\u0000'

byte

0

boolean

false

double

0.0

引用数据类型

null

在准备阶段会为value分配内存并赋初值为0,在初始化阶段才会将值修改为1。

final修饰的基本数据类型的静态变量,准备阶段直接会将代码中的值进行赋值。

2.3. 解析

解析阶段主要是将常量池中的符号引用替换为直接引用,符号引用就是在字节码文件中使用编号来访问常量池中的内容。

  • 符号引用就是在字节码文件中使用编号来访问常量池中的内容。
  • 直接引用不在使用编号,而是使用内存中地址进行访问具体的数据。

3. 初始化

初始化阶段会执行字节码文件中clinit(class init 类的初始化)方法的字节码指令,包含了静态代码块中的代码,并为静态变量赋值。

3.1. 案例

public class Demo1 {
    public static int value = 1;
    static {
        value = 2;
    }
   
    public static void main(String[] args) {
    }
}
==============================================
iconst_1
putstatic #2 <init/Demo1.value : I>
iconst_2
putstatic #2 <init/Demo1.value : I>
return
1. iconst\_1,将常量1放入操作数栈
2.putstatic指令会将操作数栈上的数弹出来,并放入堆中静态变量的位置,
字节码指令中#2指向了常量池中的静态变量value,在解析阶段会被替换成变量的地址。
3.后两步操作类似,执行value=2,将堆上的value赋值为2。
res:value = 2
==============================================
public class Demo1 {
    static {
        value = 2;
    }
    public static int value = 1;
    public static void main(String[] args) {
    }
}
==============================================
iconst_2
putstatic #2 <init/Demo1.value : I>
iconst_1
putstatic #2 <init/Demo1.value : I>
return
res:value = 1
此时注意代码块,变量以及方法都为静态

  • init方法,会在对象初始化时执行
  • main方法,主方法
  • clinit方法,类的初始化阶段执行

3.2. 类初始化

clinit执行

  1. 访问一个类的静态变量或者静态方法,注意变量是final修饰的并且等号右边是常量不会触发初始化。final在连接准备阶段就已经赋值,所以不会初始化。
  2. 调用Class.forName(String className)。
  3. new一个该类的对象时。
  4. 执行Main方法的当前类。
  5. final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。

client不会执行

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接进行初始化。
  4. 数组的创建不会导致数组中元素的类进行初始化。

3.3. 面试题

/*
如下代码的输出结果是什么?
*/
public class Test1 {
    public static void main(String[] args) {
        System.out.println("A");
        new Test1();
        new Test1();
    }
    public Test1(){
        System.out.println("B");
    }
    {
        System.out.println("C");
    }
    static {
        System.out.println("D");
    }
}
D
A
C
B
C
B
//代码块在每个方法前都会加载
public class Demo01 {
    public static void main(String[] args) {
        System.out.println(A02.a);
        System.out.println(B02.a);
        new B02();
        System.out.println(B02.a);
    }
}
class A02{
    static int a = 0;
    static {
        a = 1;
    }
}
class B02 extends A02{
    static {
        a = 2;
    }
}
1
1
2
//只访问父类静态变量,只初始化父类
public class Test2 {
    public static void main(String[] args) {
        Test2_A[] arr = new Test2_A[10];
    }
}
class Test2_A {
    static {
        System.out.println("Test2 A的静态代码块运行");
    }
}
//数组的创建不会导致数组中元素的类进行初始化。
public class Test4 {
    public static void main(String[] args) {
        System.out.println(Test4_A.a);
    }
}
class Test4_A {
    public static final int a = Integer.valueOf(1);
    static {
        System.out.println("Test3 A的静态代码块运行");
    }
}
Test3 A的静态代码块运行
1
//final修饰的变量如果赋值的内容需要执行指令才能得出结果,会执行clinit方法进行初始化。
目录
相关文章
|
5天前
|
安全 Java 编译器
Java 虚拟机加载 Java 类的过程
【6月更文挑战第7天】Java 是一门编译型语言,在完成代码的编写以后,需要使用 Java 编译器将源码编译成字节码文件,供虚拟机运行。在字节码被 Java 虚拟机执行之前,需要将对应的类进行加载。
35 3
|
4天前
|
存储 XML 安全
JVM系列5-类文件结构
JVM系列5-类文件结构
6 0
|
17天前
|
监控 Java 测试技术
滚雪球学Java(45):探秘Java Runtime类:深入了解JVM运行时环境
【5月更文挑战第20天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
18 1
滚雪球学Java(45):探秘Java Runtime类:深入了解JVM运行时环境
|
20天前
|
存储 Java 程序员
【JVM】类的声明周期(加载、连接、初始化)
【JVM】类的声明周期(加载、连接、初始化)
18 1
|
24天前
|
前端开发 Java 应用服务中间件
【JVM系列笔记】类加载
类加载器分为两类,一类是Java代码中实现的,一类是Java虚拟机底层源码实现的。常见的类加载器有启动类加载器,拓展类加载器,应用类加载器以及自定义类加载器。以及类加载机制,双亲委托策略,以及打破双亲委托策略的几种方式。
57 0
|
24天前
|
存储 Arthas Java
【JVM系列笔记】字节码
本文介绍了Java虚拟机(JVM)的组成,包括类加载子系统、运行时数据区、执行引擎和本地接口。字节码文件由基础信息(如魔数和版本号)、常量池、字段、方法和属性组成。常量池用于存储字符串等共享信息,方法区则包含字节码指令。执行引擎包含解释器、即时编译器和垃圾回收器,负责字节码的解释和优化。文章还提到了字节码工具,如javap、jclasslib和Arthas,用于查看和分析字节码。
49 0
【JVM系列笔记】字节码
|
1月前
|
存储 前端开发 Java
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构
深入浅出JVM(四)之类文件结构
|
18天前
|
Java Linux
JVM堆内存诊断
JVM堆内存诊断
18 0
|
4天前
|
存储 监控 算法
【JVM】如何定位、解决内存泄漏和溢出
【JVM】如何定位、解决内存泄漏和溢出
12 0
|
4天前
|
算法 安全 Java
JVM系列4-垃圾收集器与内存分配策略(二)
JVM系列4-垃圾收集器与内存分配策略(二)
10 0
JVM系列4-垃圾收集器与内存分配策略(二)