【从零开始学习JVM | 第三篇】类的生命周期(高频面试)

前言:

在Java编程中,类的生命周期是指类从被加载到内存中开始,到被卸载出内存为止的整个过程。了解类的生命周期对于理解Java程序的运行机制以及性能优化非常重要。

在本文中,我们将深入探讨类的生命周期,从类加载到内存中的各个阶段,以及在这个过程中发生的一些关键事件和操作。我们将了解类的加载、链接和初始化过程,以及类在内存中的存储结构和引用方式。

目录

前言:

类的生命周期概述:

杂项知识点:

总结:


类的生命周期概述:

在Java中,类的生命周期较为复杂,涉及到加载链接初始化使用卸载几个主要阶段。下面详细解释这些阶段:

  1. 加载(Loading): 这是类的生命周期的第一个阶段。在这个阶段,Java虚拟机(JVM)将类的.class文件从硬盘读入内存,为之创建一个 java.long.class 对象,并且在方法区中生成一个 InstanceKlass 对象来存储类的基本信息。而开发者只能操作java.long.class 对象而不能操作InstanceKlass 对象 需要注意的是:静态字段的数据存储在java.long.class中。这两个对象是关联的,可以相互找到。类的加载通常是由类加载器完成的,包括引导类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)和系统类加载器(System ClassLoader),还可以有用户自定义的类加载器。

  2. 链接(Linking): 加载完成后,类进入链接阶段,这个阶段又分为验证、准备和解析三个子阶段。

    • 验证(Verification):确保被加载的类符合JVM规范,没有安全问题。
    • 准备(Preparation):为类变量分配内存,并设置类变量的默认初始值 。并且需要注意的是:final修饰的基本数据类型的静态变量,准备阶段会直接将代码中的值进行赋值。
    • 解析(Resolution):将类、接口、字段和方法的符号引用转换为直接引用。
  3. 初始化(Initialization): 链接阶段之后,类会被初始化。在这个阶段,JVM 负责执行类的静态初始化器和静态初始化块。这包括执行静态字段的赋值语句和静态代码块。初始化是在类首次被使用时触发的,例如实例化、访问静态字段或调用静态方法时。

  4. 使用(Using): 类的初始化之后,它就可以在程序中自由使用了。这包括创建实例、调用方法和访问字段等操作。在这个阶段,对象会被创建和操作,它们各自也会经历自己的生命周期。

  5. 卸载(Unloading): 在某些情况下,当一个类不再需要时,它会被卸载。类的卸载发生在垃圾收集的过程中,当确定某个类的Class对象不再被引用,且对应的ClassLoader实例也不再存在时,JVM就可能卸载这个类。但是,在常见的Java应用中,由于系统类加载器加载的类一直会被引用,所以这些类通常只有在JVM停止运行时才会被卸载。

需要注意的是,Java中的类卸载并不是很常见,因为大多数应用的生命周期内,其加载的类都会一直被使用。只有在某些特定的场景下,比如热部署、动态加载和卸载插件的应用服务器等环境中,类的卸载才是必要的。

杂项知识点:

1.类加载器的种类:

引导类加载器(Bootstrap ClassLoader):它是虚拟机的一部分,负责加载Java核心库(JAVA_HOME/jre/lib/rt.jar里面的类或-Xbootclasspath参数指定的路径中的类)。

扩展类加载器(Extension ClassLoader):它负责加载JAVA_HOME/jre/lib/ext目录中或java.ext.dirs系统属性指定路径中的类库。

系统类加载器(System ClassLoader):它根据Java应用的类路径(CLASSPATH)来加载Java应用类。

用户自定义加载器(User-Defined ClassLoaders):Java允许开发者通过继承java.lang.ClassLoader类的方式实现自己的类加载器。

2.什么是方法区:

方法区(Method Area)是Java虚拟机(JVM)中的一块内存区域,存储了已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。方法区不同于堆(Heap)和栈(Stack),它是线程共享的,存储的是类相关的数据而不是对象实例。

需要注意的是,Java 8及之前的版本中,方法区是位于永久代(Permanent Generation)内的。而从Java 8开始,永久代被元空间(Metaspace)取代,方法区的数据被存储在元空间中。元空间是使用堆内存来实现的,但是它与Java堆是独立分配和回收的,因此方法区并不属于堆或栈。

3.哪几种情况会导致类的初始化:

  1. 创建类的实例:当首次创建某个类的实例时,该类将被初始化。
  2. 访问类的静态方法:首次调用类的任意一个静态方法时,该类将被初始化。
  3. 访问类或接口的静态字段:首次访问类或接口的静态字段(除了常量字段)时,该类或接口将被初始化。
  4. 反射:使用反射方式调用Class.forName()时,如果指定了要进行初始化,则会触发类的初始化。
  5. 初始化子类:如果一个类还未被初始化,则在其任何子类被初始化时,该父类也会被初始化。
  6. Java虚拟机启动时:定义了main方法的那个类将在程序开始执行时被初始化。
  7. 动态语言支持:如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  8. 接口的默认方法:如果接口定义了默认方法,并且在首次调用其实现类的任何默认方法时,该接口会被初始化。
  9. 子类的初始化clinit调用之前,会先调用父类的clinit初始化方法。

4.哪几种方式不会导致类的初始化

  1. 无静态代码块且无静态变量赋值语句。
  2. 有静态变量的声明,但是没有赋值语句。
  3. 静态变量的定义使用final关键字,这类变量会在准备阶段直接初始化 。
  4. 直接访问父类的静态变量,不会触发子类的初始化。
  5. 数组的创建不会导致数组中元素的类进行初始化。

面试题实战:

说出以下代码运行结果:

java 复制代码
public class test5 {
    public static void main(String[] args) {

        System.out.println("A");
        new test5();
        new test5();
    }
    public  test5()
    {
        System.out.println("B");
    }
    {
        System.out.println("C");
    }
    static {

        System.out.println("D");
    }

}
  1. 首先,在类加载过程中,**会执行静态代码块中的代码。**因此,静态代码块中的输出语句System.out.println("D");会被执行一次,打印出"D"。
  2. 接着,在main方法中,首先输出字符"A"
  3. 然后创建了第一个test5对象。在创建对象时,首先会执行实例代码块中的代码 。因此,实例代码块中的输出语句System.out.println("C");会被执行一次,打印出"C"。接着,执行构造方法test5()中的代码,打印出"B"。
  4. 再创建第二个test5对象时,同样会执行实例代码块中的代码,打印出"C"。接着,执行构造方法test5()中的代码,打印出"B"。。

因此打印结果为DACBCB,你做对了吗?

说出以下运行结果:

java 复制代码
public class Demo01 {
    public static void main(String[] args) {
        new B02();
        System.out.println(B02.a);
    }
}
class A02
{
    static int a=0;
    static{
        System.out.println("A02");
        a=1;
    }
}
class B02 extends A02{
    static{
        System.out.println("B02");
        a=2;
    }
}

首先执行main方法,在main方法中创建了一个B02对象,由于B02继承自A02,因此会先初始化A02类。在A02类的静态代码块中,会输出"A02"并将a的值赋为1。接着初始化B02类,在B02类的静态代码块中,会输出"B02"并将a的值赋为2。最后,打印输出B02.a的值,即为2。

因此打印结果为:A02 B02 2,你做对了吗?

说出以下结果:

java 复制代码
public class Demo01 {
    public static void main(String[] args) {

        System.out.println(B02.a);
    }
}
class A02
{
    static int a=0;
    static{
        System.out.println("A02");
        a=1;
    }
}
class B02 extends A02{
    static{
        System.out.println("B02");
        a=2;
    }
}

运行结果为A02 1 原因是因为:访问父类的静态变量,只初始化父类

总结:

类的生命周期从加载、验证、准备、解析到初始化,每个阶段都有特定的任务和目标。在加载阶段,类的字节码数据被加载到内存中,并存储在方法区中。验证阶段验证类的字节码的正确性和安全性。准备阶段为静态变量分配内存并设置默认初始值,也将静态变量存储在方法区中。解析阶段将符号引用解析为直接引用,以便后续的方法调用。最后,在初始化阶段初始化类的静态变量和执行静态代码块的逻辑。

通过了初始化阶段的类可以进入正常的使用阶段,在这个阶段中,类的实例可以被创建,实例变量和方法可以被调用。但是,我们也要意识到类的生命周期并不只局限于这些阶段。类的卸载是一个不确定的阶段,只有当类的加载器认为它不再需要时,类才会被卸载。此外,类的生命周期还受到一些因素的影响,例如类的引用是否存在,是否被其他对象引用等。

了解类的生命周期对于理解Java程序的运行机制非常重要。它帮助我们理解类的加载、初始化和使用过程,并在必要时进行优化和资源管理。同时,深入了解类的生命周期也有助于我们编写更高效、可靠的Java应用程序。

总之,类的生命周期从加载到卸载,经历了多个阶段,每个阶段都有特定的任务和目标。理解类的生命周期有助于我们更好地理解和管理Java程序的运行机制。

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!

相关推荐
Derek_Smart43 分钟前
从一次 OOM 事故说起:打造生产级的 JVM 健康检查组件
java·jvm·spring boot
Lee川3 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i5 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有5 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有5 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫6 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫6 小时前
Handler基本概念
面试
Wect7 小时前
浏览器缓存机制
前端·面试·浏览器
大道至简Edward7 小时前
深入 JVM 核心:一文读懂 Class 文件结构(附 Hex 实战解析)
jvm
掘金安东尼7 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试