JVM工作原理与实战(七):类的生命周期-初始化阶段

简介: JVM作为Java程序的运行环境,其负责解释和执行字节码,管理内存,确保安全,支持多线程和提供性能监控工具,以及确保程序的跨平台运行。本文主要介绍了类的生命周期、类的初始化阶段等内容。

一、类的生命周期

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


1.加载(Loading)

加载阶段是类的生命周期的起始点。当应用程序首次需要使用某个类时,Java虚拟机(JVM)会负责加载这个类。加载是通过类的加载器(ClassLoader)完成的,它会查找并加载类的二进制数据。这个过程包括将类的字节码从文件系统、JAR文件或网络加载到内存中。

2.连接(Linking)

连接阶段是加载阶段的后续,它包括验证、准备和解析三个子阶段。

  • 验证(Verification):验证阶段主要是确保被加载的类文件数据符合JVM规范,没有安全方面的隐患,以及是否与应用程序的其它部分兼容。验证过程包括文件格式验证、元数据验证、字节码验证和符号引用验证。
  • 准备(Preparation):准备阶段是为类的静态变量分配内存,并设置默认的初始值。需要注意的是,准备阶段并不会执行任何初始化操作。
  • 解析(Resolution):解析阶段是将符号引用转换为直接引用。在Java中,符号引用是一个类的全限定名,而直接引用是一个直接指向内存中的地址的指针。解析阶段发生在运行时,而不是编译时。

3.初始化(Initialization)

初始化阶段是类加载过程中的最后一步,当准备和解析阶段完成后,JVM会执行类的构造器方法,这个方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块集合来的。需要注意的是,构造器方法中的代码只在类被首次使用时执行一次。

4.使用(Using)

一旦类被成功加载、连接并初始化后,就可以被实例化并用于执行应用程序的业务逻辑。在应用程序运行期间,类可能会被频繁地使用。

5.卸载(Unloading)

当应用程序不再需要某个类时,该类的实例以及与其相关的资源将会被回收,这个过程就是卸载。但是需要注意的是,只有当一个类不再被任何活动对象所引用时,它才会被卸载。另外,JVM的垃圾回收机制(Garbage Collection, GC)负责自动处理类的卸载和资源的回收。

二、初始化阶段

1.初始化阶段分析

初始化阶段是类生命周期中的关键阶段之一,主要涉及静态代码块的执行和静态变量的初始化。这个阶段是在类首次被加载到内存中或者在首次实例化这个类的对象时发生的。

在初始化阶段,会执行静态代码块中的代码,这些代码块在类定义中以static声明的部分。这些代码块仅在类首次被加载时执行一次,主要用于执行一些仅需在类加载时进行的初始化操作,如静态变量的赋值等。

此外,初始化阶段会执行字节码文件中clinit部分的字节码指令。clinit方法是Java编译器自动生成的特殊方法,用于执行所有类级别的初始化操作,包括对静态变量的赋值、静态代码块的执行等。clinit方法的执行顺序与Java源代码中编写的顺序一致,确保了按照源代码的顺序进行初始化。

案例:

public class Demo1 {
    public static int value = 1;
    static {
        value = 2;
    }
    public static void main(String[] args) {
    }
}

image.gif

字节码信息:


<init> 构造方法
main Main方法
<clinit> 初始化阶段执行

clinit部分的字节码指令:

image.gif

0 iconst_1
1 putstatic #2 <init/Demo1.value : I>
4 iconst_2
5 putstatic #2 <init/Demo1.value : I>
8 return

image.gif

指令解析:

iconst_1 将常量1放入操作数栈
putstatic #2 <init/Demo1.value : I> 从操作数栈中获取值设置到静态变量中

初始化步骤:

image.gif

2.类的初始化触发方式

类的初始化可以通过以下几种方式触发:

  • 访问类的静态变量或静态方法。当程序首次访问类的静态变量或静态方法时,会触发类的初始化。需要注意的是,如果静态变量是final修饰的并且等号右边是常量,则不会触发初始化。

案例:

public class Demo1 {
    public static void main(String[] args) {
        int i = Test.i;
        System.out.println(i);
    }
}
class Test{
    static {
        System.out.println("初始化");
    }
    public static int i = 0;
}

image.gif

添加-XX:+TraceClassLoading 参数可以打印出加载并初始化的类。

-XX:+TraceClassLoading

image.gif

image.gif

image.gif

运行结果:

image.gif

  • 调用Class.forName(String className)方法。这个方法用于动态加载类,当调用该方法时,会触发类的初始化。

Class.forName(String className) 源码:

@CallerSensitive
    public static Class<?> forName(String var0) throws ClassNotFoundException {
        Class var1 = Reflection.getCallerClass();
        return forName0(var0, true, ClassLoader.getClassLoader(var1), var1);
    }
    @CallerSensitive
    public static Class<?> forName(String var0, boolean var1, ClassLoader var2) throws ClassNotFoundException {
        Class var3 = null;
        SecurityManager var4 = System.getSecurityManager();
        if (var4 != null) {
            var3 = Reflection.getCallerClass();
            if (VM.isSystemDomainLoader(var2)) {
                ClassLoader var5 = ClassLoader.getClassLoader(var3);
                if (!VM.isSystemDomainLoader(var5)) {
                    var4.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(var0, var1, var2, var3);
    }
    private static native Class<?> forName0(String var0, boolean var1, ClassLoader var2, Class<?> var3) throws ClassNotFoundException;

image.gif

boolean var1参数表示是否初始化这个类:


案例:

public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> demo2 = Class.forName("Test.Demo2");
    }
}
class Demo2 {
    static {
        System.out.println("初始化");
    }
}

image.gif

运行结果:


  • 创建一个类的对象。当使用new关键字创建一个类的对象时,会触发类的初始化。
  • 执行Main方法的当前类。当Java应用程序的入口点是Main方法时,会自动触发当前类的初始化。

案例:

public class Demo3 {
    static {
        System.out.println("Demo3初始化");
    }
    public static void main(String[] args) throws ClassNotFoundException {
        new Demo4();
    }
}
class Demo4{
    static {
        System.out.println("Demo4初始化");
    }
}

image.gif

运行结果:


3.不执行初始化指令的特殊情况

在字节码层面,初始化阶段是通过执行clinit方法来完成的。然而,值得注意的是,clinit方法并不是在所有情况下都会出现。以下是一些特定情况下不会执行clinit方法的情况:

  • 如果类中既没有静态代码块,也没有对静态变量进行赋值的语句,那么在加载类时不会执行clinit方法。这是因为clinit方法的目的是执行类级别的初始化操作,而在这种情况下,没有需要进行的初始化工作。

案例:

public class Demo1 {
    public static void main(String[] args) {
        
    }
}

image.gif

字节码信息:


  • 如果类中有静态变量的声明,但没有对其进行赋值的语句,同样不会执行clinit方法。静态变量的初始化需要通过赋值语句来进行,如果只是声明但没有赋值,则不会被初始化。

案例:

public class Demo1 {
    public static int i;
    public static void main(String[] args) {
    }
}

image.gif

 字节码信息:


  • 如果静态变量的定义使用了final关键字,这类变量会在准备阶段直接进行初始化,因此在类加载时不会执行clinit方法。这是因为final修饰的静态变量在编译时就已经确定了值,不需要在运行时再进行初始化。

案例:

public class Demo1 {
    public static final int i = 1;
    public static void main(String[] args) {
    }
}

image.gif

 字节码信息:


4.继承关系下的初始化阶段

在继承关系下,类的生命周期中的初始化阶段变得更为复杂。子类和父类之间的初始化交互是理解这个过程的关键。

  • 直接访问父类的静态变量:在Java中,静态变量是类级别的变量,它们不属于任何一个对象实例,而是与类本身关联。当我们在子类中直接访问父类的静态变量时,只会触发父类的初始化,而不会触发子类的初始化。这是因为静态变量的初始化是类级别的操作,与子类的实例化过程无关。这种机制确保了父类静态变量的初始化在子类中使用之前已经完成。
  • 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法:在Java字节码层面,类的初始化是通过执行clinit方法完成的。当一个子类被初始化时,它的clinit方法会被调用。但在子类的clinit方法执行之前,父类的clinit方法会被先调用。这种顺序确保了父类中的静态变量和静态代码块在子类使用之前已经完成初始化。这种机制确保了继承关系下的正确初始化顺序,使得子类可以依赖于父类中定义的静态变量和静态代码块。

总结

JVM是Java程序的运行环境,负责字节码解释、内存管理、安全保障、多线程支持、性能监控和跨平台运行。本文主要介绍了类的生命周期、类的初始化阶段等内容,希望对大家有所帮助。

相关文章
|
1天前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
17 0
|
1天前
|
Oracle Java 关系型数据库
JVM深入原理(一+二):JVM概述和JVM功能
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行。
18 0
|
1天前
|
Arthas 存储 Java
JVM深入原理(三+四):JVM组成和JVM字节码文件
目录3. JVM组成3.1. 组成-运行时数据区3.2. 组成-类加载器3.3. 组成-执行引擎3.4. 组成-本地接口4. JVM字节码文件4.1. 字节码文件-组成4.1.1. 组成-基础信息4.1.1.1. 基础信息-魔数4.1.1.2. 基础信息-主副版本号4.1.2. 组成-常量池4.1.3. 组成-方法4.1.3.1. 方法-工作流程4.1.4. 组成-字段4.1.5. 组成-属性4.2. 字节码文件-查看工具4.2.1. javap4.2.2. jclasslib4.2.3. 阿里Arthas
12 0
|
1天前
|
存储 安全 Java
JVM深入原理(五):JVM组成和JVM字节码文件
类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析。
11 0
|
1天前
|
Arthas Java 测试技术
JVM深入原理(六)(一):JVM类加载器
目录6. JVM类加载器6.1. 类加载器-概述6.2. 类加载器-执行流程6.3. 类加载器-分类(JDK8)6.3.1. JVM底层实现的类加载器6.3.1.1. 启动类加载器6.3.2. Java代码实现类的加载器6.3.2.1. 扩展类加载器6.3.2.2. 应用程序类加载器6.4. 类加载器-Arthas查看类加载器
8 0
|
1天前
|
Java 关系型数据库 MySQL
JVM深入原理(六)(二):双亲委派机制
自定义类加载器打破双亲委派机制的方法:复写ClassLoader中的loadClass方法常见问题:要加载的类名如果是以java.开头,则会抛出安全性异常加载自定义的类都会有一个共同的父类Object,需要在代码中交由父类加载器去加载自定义类加载器不手动指定parent会默认指定应用类加载两个自定义类加载器加载同一个类会被认为是两个对象,只有相同的类加载器+想通的类限定名才会被认为是一个对象。
9 0
|
1天前
|
存储 安全 Java
JVM深入原理(七)(一):运行时数据区
栈的介绍:Java虚拟机栈采用栈的数据结构来管理方法调用中的基本数据,先进后出,每一个方法的调用使用一个栈帧来保存栈的组成:栈:一个线程运行所需要的内存空间,一个栈由多个栈帧组成栈帧:一个方法运行所需要的内存空间活动栈帧:一个线程中只能有一个活动栈帧栈的生命周期:栈随着线程的创建而创建,而回收会在线程销毁时进行栈的执行流程:栈帧压入栈内执行方法执行完毕释放内存若方法间存在调用,那么会压入被调用方法入栈,执行完后释放内存,再执行当前方法,直到执行完毕,释放所有内存。
9 0
|
25天前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
66 6
|
2月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
205 29
JVM简介—1.Java内存区域
|
4月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
763 166