深入JVM:从类加载机制解读类的生命周期


深入JVM:从类加载机制解读类的生命周期

一、序言

对于Java工程师而言,深入理解JVM(Java虚拟机)不仅是掌握Java程序运行机制的基础,也是提升系统性能、优化应用和解决复杂问题能力的重要一步,更是Java进阶之路的重中之重。

本文小豪将带大家学习类的生命周期,包括类的验证、准备、解析和初始化这几个关键步骤,同时从类加载的角度剖析static静态代码、构造方法、初始化方法的执行顺序,进一步理解类加载机制。

二、前置知识

在学习类的生命周期之前,大家有必要先行了解一些基础概述:

(1)类的字节码文件

类的字节码文件即.class文件,后缀为.class的文件是Java编译器编译Java源文件(.java)后产生的目标文件。

.class文件包含了字节码指令,使其能够在JVM上解释执行。

(2)JVM内存模型

即JVM管理的内存,也称运行时数据区,负责管理JVM使用到的内存,主要包括虚拟机栈、本地方法栈、程序计数器、方法区和堆。

想必大家稍微接触过JVM都应该有所了解,如果还有不知道在讲什么的小伙伴,可以先行恶补一下JVM基础

三、生命周期

类的生命周期描述了一个类加载、使用、卸载的整个过程。

大致分为五个阶段:加载(Loading)连接(Linking)初始化(Initialization)使用(Using)卸载(Unloading)。其中连接阶段又分为验证准备解析三个步骤。

在这里插入图片描述

1、加载Loading

加载阶段是类生命周期的起点,负责将Java类的字节码文件加载到JVM内存中,其主要通过类加载器完成。

主要业务流程如下:

  • 类加载器根据类的全限定类名从多个渠道获取类的字节码信息(二进制数据流)

  • 加载完成之后会为其分配两块内存空间,分别在方法区和堆上

    1. 方法区(JDK1.8之后称元空间):存储类的字节码信息,生成InstanceKlass对象,保存类的所有信息,包括类基本信息、静态常量池、字段、方法、虚方法表(多态)
    2. 创建并存储类的Class对象,用来封装在方法区中类的数据结构,用于对外暴露提供

基于安全性考虑,JVM只提供给我们堆中类对象的访问权限,方法区中的类信息是无法访问的,同时堆中的类对象也是实现反射的入口

流程图如下:

在这里插入图片描述

2、连接Linking

(1)验证Verification

连接阶段的第一个步骤是验证,主要是确保加载阶段加载的Java字节码信息是符合规范要求的,对字节码文件进行严格的检查,检测Java字节码文件是否符合《Java虚拟机规范》中的约束。

其具体过程不用深究,小豪在这里大致总结一下:

  • 文件格式检查:基本文件信息正确性检查
    1. 检查字节码文件的基本格式,是否以魔数0xCAFEBABE开头(.class文件标识)
    2. 检查字节码文件的主次版本号,是否与当前Java环境匹配
  • 元数据检查:语义规范正确性检查
    1. 检查类的继承关系、接口实现,是否存在父类(类默认继承Object
    2. 检查访问权限的合法性,是否存在被final修饰的类或方法被重写
  • 字节码检查:字节码规范正确性检查
    1. 检查数据类型安全,方法的入参数据类型是否正确,变量赋值的数据类型是否正确,确保操作数栈和局部变量表存在正确的数据类型
  • 符号引用检查:符号引用有效性检查
    1. 检查类文件中的符号引用是否有效,是否存在当前类引用的其它类或者方法,当前类是否有权限访问引用的类或方法(例如检查是否访问其它类中private修饰的方法)
(2)准备Preparation

准备主要是为类的静态变量分配内存,并为其设置默认值

其中数值类型的默认为0boolean类型默认为fasle,引用类型默认为null

(重要)如果静态变量被final修饰,其会被认为是常量,对应的值在编译时就已经确定,JVM会直接为其赋定义的值,而不会赋默认值。

结合案例我们来看一下:

这里查看类编译后的字节码文件,小豪使用的是Jclasslib工具,通过IDEA集成Jclasslib插件。

在这里插入图片描述

Jclasslib字节码编辑器是一个可视化已编译Java类文件和包含的字节码的工具,更多用法大家可自行查阅,这里不过多赘述

情况一:静态变量未被final修饰

public static String name = "xiaohao";
public static int age = 24;

Jclasslib字节码工具呈现结果:

在这里插入图片描述

如图,静态变量nameage分别被赋予默认值null0

情况二:静态变量被final修饰

public static final String name = "xiaohao";
public static final int age = 24;

Jclasslib字节码工具呈现结果:

在这里插入图片描述

此时由于静态变量nameagefinal修饰,在准备环节直接完成赋值,分别被赋予定义的xiaohao24

(3)解析Resolution

连接阶段的最后一步就是解析了,主要是将类中的符号引用转换为直接引用

说白了,符号引用类似于数据库中的非聚簇索引,想要获取一条数据,需要查询两次,先获取主键,再通过主键获取完整数据。直接引用类似于聚簇索引,直接通过主键获取了完整数据。

直接引用直接指向了内存中的某一个具体地址,可以直接定位到目标,效率更高,进一步降低了内存开销。

3、初始化Initialization

上述加载、连接步骤都正常之后,进入初始化阶段。执行类的初始化方法<clinit>

主要是完成静态代码块的执行并且为静态变量赋值。

类执行初始化通常包含以下几种方式:

  • 使用new关键字创建对象实例

  • 访问类的静态变量或静态方法

  • 对类进行反射操作

  • 执行Main方法所在的类

这里需要注意一下,子类调用初始化方法之前,会优先调用父类的初始化方法,如果父类存在静态代码块,也会优先执行

同时初始化方法<clinit>的执行顺序与我们编写的代码顺序是一致的。

举个例子:

public class UserInfo {

    // 定义静态变量放在静态代码块之前
    public static int age = 24;

    static {
        age = 25;
    }
}

打印UserInfo.age,控制台输出:

25

调换静态变量定义与静态代码块顺序:

public class UserInfo {
    
    // 静态代码块放在定义静态变量之前
    static {
        age = 25;
    }

    public static int age = 24;
}

控制台输出:

24

查看字节码指令,也验证了初始化方法<clinit>的执行顺序与代码流程保持一致:

在这里插入图片描述

4、使用Using

经历上述阶段,代表类已经正式成型了,可以进行使用了,执行具体的业务逻辑。

5、卸载Unloading

类生命周期最后一个阶段即为类的卸载,卸载条件相对苛刻,其需要同时满足三个条件:

  1. 类的所有实例对象都已经被垃圾回收,堆内存中不存在类对象及其子类对象。
  2. 加载此类的类加载器也被回收(运行期间基本不太可能)。
  3. 类对应的Class对象没有在任何地方被引用,即无法使用反制机制获取到该类信息。

五、经典案例

接下来小豪结合几个大厂经典笔试题,带大家深入理解一下类的生命周期:

(1)No.1

public class JvmStudy {

    // 构造方法
    public JvmStudy(){
        System.out.println("B");
    }

    // 初始化块
    {
        System.out.println("C");
    }

    // 静态代码块
    static {
        System.out.println("D");
    }

    public static void main(String[] args) {
        System.out.println("A");
        new JvmStudy();
        new JvmStudy();
    }
}

控制台输出:

DACBCBD

解析:

  1. 执行main方法,自动触发当前类的初始化,JVM首先会先加载JvmStudy类并初始化,触发静态代码块的执行,输出单词D
  2. 其次执行main方法首行代码,打印单词A
  3. 最后创建了两个JvmStudy对象,由于该类已经被加载,不会再打印单词D,依次执行类的初始化块和构造方法,输出单词CBCB

(2)No.2

public class JvmStudy {
    public static void main(String[] args) {
        // 调用Cat类的静态变量catSay
        System.out.print(Cat.catSay);
    }
}

class Animal{
    public static String animalSay = "I'm Animal";

    static {
        System.out.println("Animal静态代码块");
    }
}

// Cat类继承Animal父类
class Cat extends Animal{
    public static String catSay = "I'm Cat";

    static {
        System.out.println("Cat静态代码块");
    }
}

控制台输出:

Animal静态代码块
Cat静态代码块
I'm Cat

解析:

  1. 在上文介绍的初始化阶段我们了解到,JVM会优先加载类的父类,由于Cat类继承自Animal父类,则会优先调用父类的初始化方法,执行Animal类的静态代码块。
  2. 其次执行自身Cat类的静态代码块

(3)No.3

public class JvmStudy {
    public static void main(String[] args) {
        // 修改点一:Cat.catSay -> Cat.animalSay
        System.out.print(Cat.animalSay);
    }
}

class Animal{
    // 修改点二:修饰符final
    public static final String animalSay = "I'm Animal";

    static {
        System.out.println("Animal静态代码块");
    }
}

class Cat extends Animal{
    public static String catSay = "I'm Cat";

    static {
        System.out.println("Cat静态代码块");
    }
}

控制台输出:

I'm Animal

解析:

此时animalSay静态变量已经被final修饰,其会被认为是常量,对应的值在编译时就已经确定,JVM会直接为其赋值,直接被打印输出,不会进行初始化AnimalCat类。

(4)No.4

public class JvmStudy {
    public static void main(String[] args) {
        // 创建B对象
        new B();
    }
}

class A {
    public A() {
        System.out.println("class A");
    }

    {
        System.out.println("I'm A class");
    }

    static {
        System.out.println("class A static");
    }
}

// B类继承自A类
class B extends A {
    public B() {
        System.out.println("class B");
    }

    {
        System.out.println("I'm B class");
    }

    static {
        System.out.println("class B static");
    }
}

控制台输出:

class A static
class B static
I'm A class
class A
I'm B class
class B

解析:

没错,还是同样的思路

  1. 实例化B对象时,先加载B的父类A,执行A的初始化,输出class A static
  2. 然后执行B的初始化,输出class B static
  3. 其次执行A类的实例化,依次执行A的初始化块和构造方法,最后执行B的初始化块和构造方法

六、后记

本文从类的生命周期开始介绍,过程中详解了连接阶段的三个步骤,同时结合经典案例来分析类加载的执行顺序,相信大家已经掌握了类的加载机制。

本文相对来说较易理解,更多的是偏理论层面的知识,大家看到这里的只是JVM体系中的九牛一毛,但是理解类的加载机制,对于我们后续进行Java程序的性能调优还是具有实际意义的。

未来一段时间,小豪将会持续更新JVM相关知识体系,如果大家觉得内容还不错,可以先点点关注,共同进步~

  • 16
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值