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

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 类的生命周期包括加载、连接(验证、准备、解析)、初始化、使用和卸载五个阶段。加载时,类加载器根据全限定名获取字节码,然后在方法区中创建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方法进行初始化。
目录
相关文章
|
16天前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
3月前
|
Java Perl
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
|
3月前
|
存储 安全 Java
开发与运维引用问题之JVM类加载过程如何解决
开发与运维引用问题之JVM类加载过程如何解决
22 0
|
4月前
|
存储 算法 Java
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
44 1
|
3月前
|
存储 算法 Java
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
|
4月前
|
安全 前端开发 Java
《JVM由浅入深学习【一】 》JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
《JVM由浅入深学习【一】 》JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
33 0
|
4月前
|
存储 Java 编译器
【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
【搞定Jvm面试】 面试官:谈谈 JVM 类文件结构的认识
|
4月前
|
存储 XML 安全
JVM系列5-类文件结构
JVM系列5-类文件结构
26 0
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
6天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
18 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制