类的生命周期及执行顺序

发布时间 2023-12-20 14:21:52作者: 小新成长之路

类的生命周期

一个类完整的生命周期,会经历五个阶段,分别为:加载、连接、初始化、使用卸载。其中的连接又分为验证、准备解析三个步骤。如下图所示:

简单一句话概括,类的加载机制就是:找到需要加载的类并把类的信息加载到jvm的方法区中),然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。结合jvm的内存结构会比较好理解。

加载(Loading)

类的加载方式比较灵活,总结下来有如下几种:

  1. 据类的全路径名找到相应的class文件,然后从class文件中读取文件内容(常用)
  2. 从jar文件中读取(常用)
  3. 从网络中获取:早些年十分流行的Applet。
  4. 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。
  5. 从非class文件中获取,其实这与直接从class文件中获取的方式本质

自定义类加载器也是在这个加载阶段工作的,可以自定义读取类的地方,比如:网络、硬盘、数据库等。

连接(Linking)

连接阶段包括3部分:验证、准备、解析

验证

进行类的合法性校验。会对比如字节码格式、变量与方法的合法性、数据类型的有效性、继承与实现的规范性等等进行检查,确保别加载的类能够正常的被jvm所正常运行。
验证阶段会完成以下校验:

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 ...... 等等

  1. 元数据验证

对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等

  1. 字节码验证

对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的。

  1. 符号引用验证

发生在解析阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问 ...... 等等

验证阶段不是必须的,虽然这个阶段非常重要,但是它对程序运行期没有影响,只影响类加载的时间,也就是说程序的启动耗时。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。

准备

为类的静态变量分配内存,并设为jvm默认的初值;对于非静态的变量(对象实例化时才分配内存),则不会为它们分配内存。简单说就是分内存、赋初值。注意:设置初始值为jvm默认初值,而不是程序设定。规则如下

  • 基本类型(int、long、short、char、byte、float、double)的默认值为0,boolean默认值false
  • 引用类型的默认值为null
  • 常量的默认值为我们程序中设定的值,对于final修饰的静态变量,final static int a = 100,则准备阶段中a的初值就是100,而不是0。非静态的final常量在初始化阶段赋值,比如:static int a = 5,则在准备阶段初始值就是0而非5。

在JDK8取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在Java堆中的。

解析

这一阶段的任务就是把Class文件中、常量池中的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程。

初始化(Initialization)

类初始化阶段是类加载过程的最后一步。而也是到了该阶段,才真正开始执行类中定义的java程序代码(字节码),之前的动作都由虚拟机主导。

注意这里一定要注意这是类的初始化,不是对象的初始化哦,对象的初始化也就是创建类实例的时候执行。

jvm对类的加载时机没有明确规范,但对类的初始化时机有:只有当类被直接引用的时候,才会触发类的初始化。类被直接引用的情况有以下几种:

  1. 通过以下几种方式:
    • new关键字创建对象
    • 读取或设置类的静态变量(注意:在准备阶段就已经赋值的变量,读取时不会触发初始化
    • 调用类的静态方法
  2. 通过反射方式执行1里面的三种方式;
  3. 初始化子类的时候,会触发父类的初始化;
  4. 作为程序入口直接运行时(调用main方法);
  5. 接口实现类初始化的时候,会触发直接或间接实现的所有接口的初始化。

关于类的初始化,记住两句话
1、类的初始化,会自上而下运行静态代码块或静态赋值语句,非静态与非赋值的静态语句均不执行(是在类实例化对象的时候执行)。
2、如果存在父类,则父类先进行初始化,是一个典型的递归模型。
区别于对象的初始化(实例化),类的初始化所做的一切都是基于类变量或类语句的(static修饰的),也就是说执行的都是共性的抽象信息。而我们知道,类就是对象实例的抽象。

使用(Using)

类的使用分为直接引用间接引用
直接引用与间接引用等判别条件,是看对该类的引用是否会引起类的初始化
直接引用已经在类的初始化中的有过阐述,不再赘述。而类的间接引用,主要有下面几种情况:

  1. 当引用了一个类的静态变量,而该静态变量继承自父类的话,不引起初始化
  2. 定义一个类的数组,不会引起该类的初始化;
  3. 当引用一个类的的常量时,不会引起该类的初始化

卸载((Unloading)

当类使用完了之后,类就要进入卸载阶段了。可卸载需要具备以下条件:

  1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

static关键字

static关键字修饰的数据存储在我们的方法区中的静态常量池中,static可以修饰方法、变量和代码块
static修饰方法:指定不需要实例化就可以激活的一个方法。this关键字不能在static方法中使用,静态方法中不能调用非静态方法,非静态方法可以调用静态方法。
static修饰变量:指定变量被所有对象共享,即所有实例都可以使用该变量。变量属于这个类。
static修饰代码块:通常用于初始化静态变量,静态代码块属于类。没加static的代码块认为是构造代码块

执行顺序

  • 实例化对象前,先加载类(对象载入之前,一定要是类先被载入)
  • 类(或者可以说静态变量和静态代码块)在生命周期结束前,只执行一次
  • 静态变量(属性)和静态代码块谁先声明谁先执行(同一个类中)
  • 非静态变量(属性)和非静态代码块谁先声明谁先执行(同一个类中)
  • 静态构造代码块是和类同时加载的,静态构造代码块是在实例化之后执行构造方法之前执行的,构造方法是在构造代码块执行完之后才执行的。
  • 静态方法属于类的,加载完类就可以调用静态方法(可以执行多次,注意区别于静态代码块,静态代码块只会执行一次);非静态方法是属于对象的,加载完对象就可以调用非静态方法。
  • 每创建一个对象,即每载入一个对象,非静态代码块都执行一次。执行类对象的载入之前就会调用

案例一

我们来通过一个例子来验证以下上面的观点

public class InitializeDemo {
    private static int k = 1;
    private static InitializeDemo t1 = new InitializeDemo("t1");
    private static InitializeDemo t2 = new InitializeDemo("t2");
    private static int i = print("i");
    private static int n = 99;

    {
        print("初始化块");
        j = 100;
    }

    public InitializeDemo(String str) {
        System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
        ++i;
        ++n;

    }

    static {
        print("静态块");
        n = 100;
    }

    private int j = print("j");

    public static int print(String str) {
        System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
        ++n;
        return ++i;
    }

    public static void main(String[] args) {
        InitializeDemo test = new InitializeDemo("test");
    }
}

输出结果:

1:初始化块   i=0    n=0
2:j   i=1    n=1
3:t1   i=2    n=2
4:初始化块   i=3    n=3
5:j   i=4    n=4
6:t2   i=5    n=5
7:i   i=6    n=6
8:静态块   i=7    n=99
9:初始化块   i=8    n=100
10:j   i=9    n=101
11:test   i=10    n=102

我们来逐个分析,
一开始调用main方法,main方法内实例化InitializeDemo的对象,在对象载入之前,一定要是类先被载入
所以我们先加载InitializeDemo类,加载类的同时,会加载静态变量和静态代码块,但是是按顺序执行,且只执行一次
先加载如下静态变量

private static int k = 1;

加载如下静态变量的时候,发现要去加载类,由于类已经被加载了,所以会实例化这个对象,这个对象实例化前,会执行非静态代码块和为非静态属性赋值,然后再执行构造方法,按在代码中顺序执行。

private static InitializeDemo t1 = new InitializeDemo("t1");

所以先执行非静态代码块的内容:

{
    print("初始化块");
    j = 100;
}

输出:1:初始化块 i=0 n=0

初始的时候i的值和n的值默认值是0,执行完这个方法后会变成i=1,n=1

接着为非静态属性赋值:

private int j = print("j");

输出:2:j i=1 n=1

输出时i=1,n=1,执行完这个方法后会变成i=2,n=2

然后执行构造方法

public InitializeDemo(String str) {
    System.out.println((k++) + ":" + str + "   i=" + i + "    n=" + n);
    ++i;
    ++n;

}

输出:3:t1 i=2 n=2

输出时i=2,n=2,执行完这个方法后会变成i=3,n=3

t1的实例化执行结束,接着执行t2的实例化

private static InitializeDemo t2 = new InitializeDemo("t2");

结果和上述一致,按非静态代码块和非静态属性然后构造方法方法的顺序执行
输出:
4:初始化块 i=3 n=3
5:j i=4 n=4
6:t2 i=5 n=5
两个静态属性(实例化)执行完,执行如下代码

private static int i = print("i");

输出:7:i i=6 n=6

这里执行完成后,i=7,n=7

接着执行下面的代码,此时n变成了99

private static int n = 99;

注意:执行完这行代码后,n的值就被赋成99了,i的值还是7。

接着执行静态代码块

static {
    print("静态块");
    n = 100;
}

输出:8:静态块 i=7 n=99

输出时i=7,n=99,执行完这个方法后会变成i=8,n=100

到此类加载完毕,可以看到static变量和静态代码块都按顺序执行了,然后开始实例化test对象,参考t1,t2的实例化,按非静态代码块和非静态属性然后构造方法方法的顺序执行,这里就不会再处理static相关的代码了。
输出:
9:初始化块 i=8 n=100
10:j i=9 n=101
11:test i=10 n=102

案例二

继承中的static执行顺序,看以下例子

public class Test3 extends Base {
    static {
        System.out.println("test static");
    }

    public Test3() {
        System.out.println("test constructor");
    }

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

class Base {
    static {
        System.out.println("Base static");
    }

    public Base() {
        System.out.println("Base constructor");
    }
}

输出结果:

Base static
test static
Base constructor
test constructor

执行Test3的构造方法,要先加载Test3的类加载,由于Test3继承于Base,所以他要先加载父类Base,静态代码块先执行。
则会先输出:Base static
再输出:test static
再执行子类的构造方法的时候,要先执行父类的构造方法(一般是找默认的构造方法即无参构造方法,除非在子类的构造方法里指定要调用父类的构造方案),如果是多级继承,会先执行最顶级父类的构造方法,然后依次执行各级子类的构造方法
所以再输出:Base constructor
然后输出:test constructor
结果就如上。

案例三

再举一个例子

public class MyTest {
    MyPerson person = new MyPerson("test");//这里可以理解为成员变量辅助,,要先把MyPerson先加载到jvm中

    static {
        System.out.println("test static");//1
    }

    public MyTest() {
        System.out.println("test constructor");//5
    }

    public static void main(String[] args) {//main方法在MyTest类中,使用mian方法先加载MyTest的静态方法,不调用其他,
        MyClass myClass = new MyClass();//对象创建的时候,会加载对应的成员变量
    }
}

class MyPerson {
    static {
        System.out.println("person static");//3
    }

    public MyPerson(String str) {
        System.out.println("person " + str);//4  6
    }
}

class MyClass extends MyTest {
    MyPerson person = new MyPerson("class");//这里可以理解为成员变量辅助,要先把MyPerson先加载到jvm中

    static {
        System.out.println("class static");//2
    }

    public MyClass() {
        //默认super()
        System.out.println("class constructor");//7
    }
}

输出:

test static
class static
person static
person test
test constructor
person class
class constructor

下面分析执行步骤:

  1. 先看MyTest类及其静态的变量,方法和代码块会随类的加载而开辟空间,有一个静态代码块,先执行,所以输出:test static,且此时MyTest类的其他语句不执行,此时MyTest类加载完成。
  2. 接着看main方法中调用了MyClass myClass =new MyClass(),实例化了一个MyClass类的对象,这时会先加载MyClass类,而MyClass类继承于MyTest类,在加载MyClass类前,会先加载MyTest类,但是MyTest类以及其静态的变量和静态代码块已经加载(在类的生命周期只执行一次),所以返回到子类(MyClass类)的加载,这时候会调用MyClass类的静态的变量和静态代码块。所以输出:class static
  3. MyClass类加载完后,在执行MyClass类的构造方法前,先初始化对象的成员变量(先初始化父类MyTest的成员变量),所以执行父类MyTest(类已加载过,这里就直接执行成员变量的初始化)的成员变量:MyPerson person = new MyPerson(“test”),于是会加载MyPerson类和其静态的变量和静态代码块。则先输出:person static
  4. 加载完MyPerson类和其静态的变量和静态代码块后,回到MyClass类开始执行非静态代码块和属性,由于MyClass继承了MyTest,所以会先初始化MyTest,初始化MyTest类的属性:MyPerson person = new MyPerson("test");会调用MyPerson类的有参构造方法,即输出:person test
  5. MyTest类的非静态属性和非静态的代码块执行完成后,然后接着执行父类构造方法,即输出:test constructor
  6. 父类MyTest构造方法执行结束,回到子类MyClass,子类再调用构造方法前,先初始化对象的成员变量MyPerson person = new MyPerson(“class”);,这时候会先先加载MyPerson 和其静态的变量和静态代码块,由于上述类已经加载,而且MyPerson没有非静态属性及非静态代码块,所以直接执行其有参构造方法,即输出:person class
  7. MyClass再无其他非静态属性及非静态代码块,执行无参构造方法,即输出:class constructor
  8. MyClass实例化完成,回到MyTest类,无后续代码,至此程序执行完成。

总结

使用一个类创建对象的时候,一般都是先加载类,完成类的初始化(静态变量的赋值和静态代码块的执行,按代码中的先后顺序,整个生命周期中只会执行一次),然后实例化对象,实例化对象时不再处理静态变量和静态代码块,会处理非静态属性和非静态代码块,也是按照代码中的先后顺序执行,最后才调用构造方法。如有继承关系,则按照此规则先执行父类后执行子类。
类加载顺序的三个原则是

  • 1、父类优先于子类
  • 2、属性和代码块(看先后顺序)优先于构造方法
  • 3、静态优先于非静态

整个程序执行顺序:
父类静态变量、父类静态语句块(按代码中的先后顺序执行)--> 子类静态变量、子类静态语句块(按代码中的先后顺序执行)--> 父类非静态变量、父类非静态语句块(按代码中的先后顺序执行)--> 父类构造器 --> 子类非静态变量、子类非静态语句块(按代码中的先后顺序执行)--> 子类构造器 --> 完成
来一张图更直观些。