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源代码中编写的顺序一致,确保了按照源代码的顺序进行初始化。

案例:

arduino 复制代码
public class Demo1 {
    public static int value = 1;

    static {
        value = 2;
    }

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

字节码信息:

构造方法
main Main方法
初始化阶段执行

clinit部分的字节码指令:

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

指令解析:

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

初始化步骤:

2.类的初始化触发方式

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

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

案例:

csharp 复制代码
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;
}

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

ruby 复制代码
-XX:+TraceClassLoading

运行结果:

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

Class.forName(String className) 源码:

java 复制代码
    @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;

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

案例:

arduino 复制代码
public class Demo1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class<?> demo2 = Class.forName("Test.Demo2");
    }
}

class Demo2 {
    static {
        System.out.println("初始化");
    }
}

运行结果:

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

案例:

csharp 复制代码
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初始化");
    }
}

运行结果:

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

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

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

案例:

typescript 复制代码
public class Demo1 {
    public static void main(String[] args) {
        
    }
}

字节码信息:

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

案例:

arduino 复制代码
public class Demo1 {
    public static int i;
    public static void main(String[] args) {

    }
}

字节码信息:

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

案例:

arduino 复制代码
public class Demo1 {
    public static final int i = 1;
    public static void main(String[] args) {

    }
}

字节码信息:

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

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

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

总结

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

相关推荐
一只叫煤球的猫4 小时前
写代码很6,面试秒变菜鸟?不卖课,面试官视角走心探讨
前端·后端·面试
bobz9655 小时前
tcp/ip 中的多路复用
后端
bobz9655 小时前
tls ingress 简单记录
后端
皮皮林5516 小时前
IDEA 源码阅读利器,你居然还不会?
java·intellij idea
你的人类朋友6 小时前
什么是OpenSSL
后端·安全·程序员
bobz9656 小时前
mcp 直接操作浏览器
后端
前端小张同学9 小时前
服务器部署 gitlab 占用空间太大怎么办,优化思路。
后端
databook9 小时前
Manim实现闪光轨迹特效
后端·python·动效
武子康10 小时前
大数据-98 Spark 从 DStream 到 Structured Streaming:Spark 实时计算的演进
大数据·后端·spark
该用户已不存在10 小时前
6个值得收藏的.NET ORM 框架
前端·后端·.net