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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 Tair(兼容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方法进行初始化。
目录
相关文章
|
2月前
|
安全 Java 应用服务中间件
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
什么是类加载器,类加载器有哪些;什么是双亲委派模型,JVM为什么采用双亲委派机制,打破双亲委派机制;类装载的执行过程
JVM常见面试题(三):类加载器,双亲委派模型,类装载的执行过程
|
26天前
|
缓存 前端开发 Java
JVM知识体系学习二:ClassLoader 类加载器、类加载器层次、类过载过程之双亲委派机制、类加载范围、自定义类加载器、编译器、懒加载模式、打破双亲委派机制
这篇文章详细介绍了JVM中ClassLoader的工作原理,包括类加载器的层次结构、双亲委派机制、类加载过程、自定义类加载器的实现,以及如何打破双亲委派机制来实现热部署等功能。
37 3
|
26天前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
35 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
4月前
|
Java Perl
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
JVM内存问题之如何统计在JVM的类加载中,每一个类的实例数量,并按照数量降序排列
|
5月前
|
存储 算法 Java
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
技术笔记:JVM的垃圾回收机制总结(垃圾收集、回收算法、垃圾回收器)
57 1
|
4月前
|
存储 安全 Java
开发与运维引用问题之JVM类加载过程如何解决
开发与运维引用问题之JVM类加载过程如何解决
29 0
|
4月前
|
存储 算法 Java
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
JAVA程序运行问题之Java类加载到JVM中加载类时,实际上加载的是什么如何解决
|
5月前
|
安全 前端开发 Java
《JVM由浅入深学习【一】 》JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
《JVM由浅入深学习【一】 》JVM由简入深学习提升(类加载过程+父子类加载过程+类加载器+双亲委派机制)
39 0
|
26天前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
36 4
|
26天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
51 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS