总结:《JVM虚拟机类加载过程与类初始化顺序》

发布时间:2024年01月04日

一·触发类加载与类初始化的条件操作:

  1. 第一次创建类的实例(第一次使用 new 关键字)。

  2. 第一次访问类的静态变量或者为静态变量赋值(但 final 常量除外,它们在编译期就已经被赋值了)。

  3. 第一次调用类的静态方法。

  4. 第一次使用反射 API 对类进行反射调用。

  5. 第一次初始化一个类的子类(会首先初始化父类)。

注意:
(1)若代码中没有显式调用某个Java类加载器去加载该类,则在触发该类初始化时,jvm底层会自动找到该类适应的类加载器,进行相应类加载与类初始化
(2)类加载过程一般都只会在第一次引用该类的时候触发,若已经加载过该类到虚拟机内存里面,即使满足上面条件操作也不会再次执行类加载
(3)类初始化是类加载的最后阶段部分,却也是两个独立的执行过程。即,可以实现只加载类到内存但不初始化类,但要初始化类必须先加载类到内存里面
(4)如果该类有父类,则类加载器会先去查找其父类,且依次类推直到最顶级的类,然后再顺序往下执行每个类的类加载具体过程
(5)一个类加载过程是包括加载、验证、准备、解析、初始化这五个小阶段
(6)即,只有等父类的类加载5个阶段执行完,才会执行子类的类加载5个阶段

二·类加载实际分为五个阶段:

1. 加载(Loading):

当 JVM 首次遇到需要使用某个类的时候,它会通过类加载器(ClassLoader)找到该类的 .class 文件。类加载器负责从指定的位置(如文件系统、网络、jar 包等)读取字节码数据到内存里面。

注意:
(1)此阶段会将类中的静态方法、非静态方法都会被加载到内存方法区中进行存储,方便后续调用,什么时候调用则什么时候触发方法操作

2. 验证(Verification):

加载完字节码后,JVM 会对字节码进行验证,确保其符合 Java 虚拟机规范,没有安全方面的问题。验证包括语法校验、元数据验证、字节码验证和符号引用验证等步骤。

3. 准备(Preparation):

在这个阶段,JVM 会为类的静态变量分配内存,并将其初始化为默认值,这个阶段又称隐式初始化。例如,对于整型静态变量,默认值为0;对于对象引用类型的静态变量,默认值为 null。这里不包含final修饰的静态变量,因为final修饰的静态变量是在编译期分配。

注意:
(1)此阶段只会对静态变量进行内存空间分配以及赋默认值;非静态变量则是在调用构造方法,触发对象实例化时,进行内存空间分配与赋默认值

4. 解析(Resolution):

解析阶段主要是将常量池中的符号引用转换为直接引用。符号引用是对类、接口、字段和方法的抽象表示,而直接引用是内存中实际的地址或偏移量。

5. 初始化(Initialization):

这个阶段又被称为显示初始化,如果该类还没有被初始化,那么 JVM 会在适当的时候触发类的初始化(参考上文触发类初始化操作)。类的初始化主要包括以下步骤:

  • 调用类的 <clinit> 方法(由编译器自动生成,用于初始化静态变量和执行静态初始化块的代码)。
注意:
(1)连续多次触发同一个类的初始化操作,jvm底层只会在第一次类加载进行类初始化,后面就不会再进行类初始化了
(2)静态变量、静态代码块,会按照它们在类中的声明顺序依次执行
(3)此阶段也只会对静态方法、静态变量、静态代码块进行操作;非静态的各种属性显式初始化不在此阶段,而是在后续调用类构造方法,对象实例化阶段进行显式初始化
(4)虽然静态变量在准备阶段已经进行隐式初始化,但是由于jvm机制原因,静态代码块中只能引用定义在其之前的静态变量;
定义在其之后的静态变量,该静态代码块中只可以对该静态变量进行赋值,却不能进行非赋值操作,否则就会报 "java: 非法前向引用" 异常。
(5)这个机制有助于提高代码的可读性和可维护性,因为在阅读代码时,开发人员可以信任变量在使用之前已经被正确地初始化。
这有助于减少代码中的错误,并使代码更容易理解和调试。

三·当一个子类具有父类时,类加载子类时,执行步骤顺序如下:

  1. 父类静态成员隐式初始化
  2. 父类静态成员显示初始化
  3. 子类静态成员隐式初始化
  4. 子类静态成员显示初始化
  5. 其他子孙类以此类推

四·触发类对象实例化的条件操作:

  1. 通过new关键字调用一个类的构造方法,创建类对象实例

五·类对象实例化分为三个阶段:

1. 隐式初始化:

成员变量按照它们在类中的声明顺序,进行内存空间分配以及自动赋予变量类型对应的默认值

2. 显示初始化:

成员变量、普通代码块,会按照它们在类中的声明顺序依次执行

注意:
(1)虽然成员变量在前面已经进行隐式初始化,但是由于jvm机制原因,普通代码块中只能引用定义在其之前的变量;
定义在其之后的成员变量,该普通代码块中只可以对该成员变量进行赋值,却不能进行非赋值操作,否则就会报 "java: 非法前向引用" 异常。
(2)这个机制有助于提高代码的可读性和可维护性,因为在阅读代码时,开发人员可以信任变量在使用之前已经被正确地初始化。
这有助于减少代码中的错误,并使代码更容易理解和调试。

3. 构造方法:

执行实际构造方法的代码逻辑

注意:
(1)虽然在源代码逻辑中是直接new一个对象,看似直接调用了某个类的构造方法,但是从调用这个构造方法开始,jvm底层会判断该类是否初次加载,
否就会先触发类加载以及类初始化(有父类先加载且初始化父类),接着再执行对象实例化(有父类先实例化父类),最后才会执行真正的构造方法代码逻辑。
(2)类初始化与类对象实例化是两个完全独立的过程,可以分开触发,但是类初始化必须在类对象实例化之前进行
(3)当然也可以在类初始化的过程中,手动触发对象实例化,这样是可以的但是不符合规范,不建议这么做

六·当一个子类具有父类,且子类父类都已经执行过类加载,则实例化子类对象时,执行步骤顺序如下:

  1. 父类非静态成员隐式初始化
  2. 父类非静态成员显示初始化
  3. 父类构造方法
  4. 子类非静态成员隐式初始化
  5. 子类非静态成员显示初始化
  6. 子类构造方法
  7. 其他子孙类依次类推

七·当一个子类具有父类时,且子类父类都没执行过类加载,则实例化子类对象时,完整执行步骤如下:

  1. 父类静态成员隐式初始化
  2. 父类静态成员显示初始化
  3. 子类静态成员隐式初始化
  4. 子类静态成员显示初始化
  5. 调用父类构造器(执行父类实例成员初始化):实际执行下面三个顺序步骤
1. 父类非静态成员隐式初始化
2. 父类非静态成员显示初始化
3. 父类构造方法
  1. 调用子类构造器(执行子类实例成员初始化):实际执行下面三个顺序步骤
1. 子类非静态成员隐式初始化
5. 子类非静态成员显示初始化
6. 子类构造方法

八·演示上述执行逻辑过程的完整代码:

注意:可以试着注释其中某些代码来测试各种情况,建议读者多多体会这两个示例代码的执行结果,从而验证上诉逻辑执行顺序是否正确

1.Father类


/**
 * @Description TODO
 * <p>
 * Copyright @ 2022 Shanghai Mise Co. Ltd.
 * All right reserved.
 * <p>
 * @Author LiuMingFu
 * @Date 2024/1/3 09:47
 */
public class Father {

    public static void main(String[] args) {
        System.out.println("父类静态main方法");
        Father father = new Father();
        System.out.println("weight=" + father.getWeight());
        System.out.println("===================================再次触发对象实例化过程===================================");
        Father father2 = new Father();
        System.out.println("===================================再次触发对象实例化过程===================================");
        Father father3 = new Father();
    }

    public Father() {
        System.out.println("父类构造方法!");
    }

    String job;

    {
        System.out.println("父类普通代码块-1,job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
        job = getJob();
    }

    static int age;
    static String name;


    static {
//        Father father = new Father();
        System.out.println("父类静态代码块-1,age=" + age + ",name=" + name);
        age = 20;
        name = "张三";
    }

    Double weight = 99.0d;

    {
        System.out.println("父类普通代码块-2,weight=" + weight + ",job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
        weight = getWeight();
    }

    static String sex = getSex();

    static {
        System.out.println("父类静态代码块-2,sex=" + sex + "age=" + age + ",name=" + name);
    }

    static Double height = getHeight();

    static {
        System.out.println("父类静态代码块-3,height=" + height + "sex=" + sex + "age=" + age + ",name=" + name);
    }

    public static String getSex() {
        System.out.println("父类静态方法-getSex");
        return "男";
    }

    public static Double getHeight() {
        System.out.println("父类静态方法-geHeight");
        return 170.0d;
    }


    public String getJob() {
        System.out.println("父类普通方法-getJob");
        return "Java工程师";
    }

    public Double getWeight() {
        System.out.println("父类普通方法-getWeight");
        return new Double("120.5");
    }
}

2.Son类


/**
 * @Description TODO
 * <p>
 * Copyright @ 2022 Shanghai Mise Co. Ltd.
 * All right reserved.
 * <p>
 * @Author LiuMingFu
 * @Date 2024/1/3 09:56
 */
public class Son extends Father{
    public static void main(String[] args) {
        System.out.println("子类静态main方法");
        Son father = new Son();
        System.out.println("weight=" + father.getWeight());
        System.out.println("===================================再次触发对象实例化过程===================================");
        Son father2 = new Son();
        System.out.println("===================================再次触发对象实例化过程===================================");
        Son father3 = new Son();
    }

    public Son() {
        System.out.println("子类构造方法!");
    }

    String job;

    {
        System.out.println("子类普通代码块-1,job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
        job = getJob();
    }

    static int age;
    static String name;


    static {
        System.out.println("=========================start-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================");
        Father father = new Father();
        Son son = new Son();
        System.out.println("=========================end-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================");
        System.out.println("子类静态代码块-1,age=" + age + ",name=" + name);
        age = 20;
        name = "李四";
    }

    Double weight = 99.0d;

    {
        System.out.println("子类普通代码块-2,weight=" + weight + ",job=" + job + ",name=" + name + ",age=" + age + ",sex=" + sex + ",height=" + height);
        weight = getWeight();
    }

    static String sex = getSex();

    static {
        System.out.println("子类静态代码块-2,sex=" + sex + "age=" + age + ",name=" + name);
    }

    static Double height = getHeight();

    static {
        System.out.println("子类静态代码块-3,height=" + height + "sex=" + sex + "age=" + age + ",name=" + name);
    }

    public static String getSex() {
        System.out.println("子类静态方法-getSex");
        return "女";
    }

    public static Double getHeight() {
        System.out.println("子类静态方法-geHeight");
        return 100.0d;
    }


    public String getJob() {
        System.out.println("子类普通方法-getJob");
        return "Python工程师";
    }

    public Double getWeight() {
        System.out.println("子类普通方法-getWeight");
        return new Double("120.5");
    }
}

3.执行Son类的main方法结果:

父类静态代码块-1,age=0,name=null
父类静态方法-getSex
父类静态代码块-2,sex=男age=20,name=张三
父类静态方法-geHeight
父类静态代码块-3,height=170.0sex=男age=20,name=张三
=========================start-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
父类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Java工程师,name=张三,age=20,sex=男,height=170.0
父类普通方法-getWeight
父类构造方法!
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=null,age=0,sex=null,height=null
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=null,age=0,sex=null,height=null
子类普通方法-getWeight
子类构造方法!
=========================end-强行在子类初始化的静态代码块中调用父类、子类对象实例化操作,可以但不规范,需要对jvm有很深的理解才建议运用=========================
子类静态代码块-1,age=0,name=null
子类静态方法-getSex
子类静态代码块-2,sex=女age=20,name=李四
子类静态方法-geHeight
子类静态代码块-3,height=100.0sex=女age=20,name=李四
子类静态main方法
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=李四,age=20,sex=女,height=100.0
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=李四,age=20,sex=女,height=100.0
子类普通方法-getWeight
子类构造方法!
子类普通方法-getWeight
weight=120.5
===================================再次触发对象实例化过程===================================
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=李四,age=20,sex=女,height=100.0
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=李四,age=20,sex=女,height=100.0
子类普通方法-getWeight
子类构造方法!
===================================再次触发对象实例化过程===================================
父类普通代码块-1,job=null,name=张三,age=20,sex=男,height=170.0
子类普通方法-getJob
父类普通代码块-2,weight=99.0,job=Python工程师,name=张三,age=20,sex=男,height=170.0
子类普通方法-getWeight
父类构造方法!
子类普通代码块-1,job=null,name=李四,age=20,sex=女,height=100.0
子类普通方法-getJob
子类普通代码块-2,weight=99.0,job=Python工程师,name=李四,age=20,sex=女,height=100.0
子类普通方法-getWeight
子类构造方法!

注意:可以试着注释其中某些代码来测试各种情况,建议读者多多体会这两个示例代码的执行结果,从而验证上诉逻辑执行顺序是否正确

九·什么时候jvm会进行class卸载?

  1. 类不再被引用:当一个类的所有实例都变为不可达(即没有任何活的对象引用该类或者其任何静态成员),并且该类的类加载器也可以被垃圾收集时,JVM可能会卸载这个类。
  2. 类加载器卸载:与类加载器的生命周期相关。每个类都与其加载它的类加载器关联。当类加载器被卸载时,它所加载的所有类也会被卸载。
  3. 堆内存压力:在运行过程中,如果JVM的堆内存压力增大,为了释放内存,JVM的垃圾收集器可能会更积极地查找并卸载不再使用的类。
  4. 明确调用System.gc():虽然不推荐,但调用System.gc()会触发垃圾收集,这可能包括类卸载的过程。然而,这并不保证一定会发生类卸载,因为具体的垃圾收集策略和行为取决于JVM实现。

注意:JVM的类卸载是一个复杂的过程,涉及到可达性分析、垃圾收集以及类加载器的管理。而且,类卸载并不频繁发生,因为大多数应用程序在运行时都会稳定在一组核心类上,这些类通常不会被卸载。此外,JVM设计时也考虑到性能和稳定性,因此它可能会保守地处理类卸载,以避免不必要的开销和风险。

十·参考文献:

1.Java非法向前引用变量

https://blog.csdn.net/hsz2568952354/article/details/97496917

文章来源:https://blog.csdn.net/weixin_48033662/article/details/135246047
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。