深入理解类的生命周期

0x00 前言

类的生命周期指的是一个字节码文件(*.class)从被JVM加载到卸载的全过程。深入学习类的生命周期有助于更好地理解我们所写的Java代码,能够分析其运行时的行为。

在这里插入图片描述
本文中我们重点关注加载、连接以及初始化三个阶段,尤其是初始化阶段可以被程序员干预,使得我们对程序有了更多的控制权。

0x01 加载阶段

(1)获取并解析字节码文件的二进制序列。除了获取本地磁盘上的字节码文件外,还可以通过解析动态代理生成的字节码文件或者来自网络的文件,其中Spring框架中的AOP技术就使用了动态代理。

在这里插入图片描述
(2)将字节码文件中的信息放到JVM中的一块专门内存区域—方法区。方法区是一块逻辑上的内存区域,取决于不同的JVM实现。

在这里插入图片描述

(3)在方法区中生成一个InstanceKlass对象,保存类的所有信息。注意这里InstanceKlass对象保存了类的所有信息。有关字节码文件的介绍可以参考我的上一篇博客。
在这里插入图片描述
(4)同时,在堆内存中生成一份与方法区类似的class对象。通常这个在堆内存的对象所包含的信息会少于方法区中的对象,而反射机制正是使用了这个堆内存中的对象,除此之外,在JDK 1.8版本之后,这个对象中还包含了类中的静态变量(在JDK 1.8之前静态变量存放在方法区中)。

可能你会有疑问,既然已经在第(3)步中创建了一个对象,为什么还要继续在堆内存中创建一个类似的对象呢?它们的主要区别在于:

  1. 方法区中的InstanceKlass对象是给JVM使用的,而JVM是使用C++编写的,所以这个InstanceKlass对象是一个由C++创建的对象;而堆内存中的对象是Java创建的对象;开发者可以访问后者但不能访问前者
  2. InstanceKlass对象会被用来实现多态,而堆内存中的对象则用于实现反射机制

在这里插入图片描述

0x02 连接阶段

连接阶段并不会执行代码,而是做一些前期的准备工作,一共可分为三个步骤:1)验证;2)准备;3)解析。
在这里插入图片描述

(1)验证阶段。验证字节码文件的内容是否满足JVM虚拟机规范。主要验证的内容有:字节码文件头部(必须为0xcafebabe)、
在这里插入图片描述

(2)准备阶段。给类的静态变量分配内存被赋值,如果静态变量被final修饰,则可以在编译阶段确定其值,因此它在内存中的值就是程序中初始化时给定的值,如果静态变量没有被final修饰,则将其赋值为对应类型的零值(zero-value)。

在这里插入图片描述

(3)解析阶段。将常量池中的符号引用替换为内存地址,为后续的初始化阶段做准备。

0x03 初始化阶段

初始化阶段会为非final的静态变量赋值,执行静态代码块。总得来说就是执行字节码文件中clinit(class init)中的字节码指令

以下图为例,clinit中会将静态变量value的值赋值为1,然后再赋值为2。
在这里插入图片描述
如何改变一下执行顺序呢?

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

此时会先将value的值赋值为2,然后再重新赋值为1。这说明一点,clinit中字节码的执行顺序和Java源代码中的执行顺序是一致的

那么类在什么时候会被初始化呢?下图给出了类被初始化的四种时机。
在这里插入图片描述
我们发现在执行类的main方法时会初始化该类,在此我们可以做一些扩展,当我们执行某个类的main方法时,会有哪些东西以何种顺序被执行呢?我们用以下代码作为例子进行讲解:

public class Test {
	public static void main(String[] args) {
		System.out.println("A");
		new Test();
		new Test();
	}

	public Test() {
		System.out.println("B");
	}

	{
		System.out.println("C");
	}

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

当我们执行Test类的main方法时,

  1. 执行Test类的clinit字节码指令,即执行Test类中的静态代码块,输出字符串"D"
  2. 执行main方法中的第一行代码,输出字符串"A"
  3. 执行构造函数,其中非静态代码块也会执行,并且先于构造函数,输出字符串"C", “B”
  4. 再一次执行构造函数,输出字符串"C", “B”

因此总的输出是"D\nA\nC\nB\nC\nB\n"。

根据之前的内容,我们知道了类的初始化阶段主要是为了(1)给非final修饰的静态变量赋值;(2)执行静态代码块中的代码,因此如果类中没有静态代码块或者非final修饰的静态变量,也就不会有clinit方法,下图给出了不产生clinit方法的几种情况。
在这里插入图片描述
之前我们讨论的是没有继承时的初始化阶段,那么如果某个类继承了另一个类,会在初始化阶段发生什么呢?其实仍然遵循之前提到的要点,如果访问的是父类的静态变量,则不会触发子类的初始化,而一旦子类的初始化阶段需要被触发,那么需要优先触发其父类的初始化阶段。
在这里插入图片描述
在以下的代码中,我们在main方法中看似访问了类B中的静态变量a,a继承自类A,因此实际上访问的是类A中的静态变量而不是类B中的静态变量,所以也只会执行类A中的初始化阶段,即将变量a先赋值为0然后再赋值为1,最终结果为1。

public class Test {
    public static void main(String[] args) {
        System.out.println(B.a);
    }
}

class A {
    static int a = 0;
    static {
        a = 1;
    }
}

class B extends A {
    static {
        a = 2;
    }
}

0x04 留给读者

在以下的代码中,我们有两个问题,

  1. 执行类Test2的main方法时,类SubTest1会被加载么,即程序是否会打印输出"“SubTest1 is initialized.” ?
  2. 在编译Test2后,字节码文件中是否存在clinit方法,即对于一个类被final修饰的静态变量,其右边是变量时,是否会将其在初始化阶段赋值?
public class Test2 {
    public static void main(String[] args) {
        SubTest1[] array = new SubTest1[10];
    }
}

class SubTest1 {
    static {
        System.out.println("SubTest1 is initialized.");
    }
}

class SubTest2 {
    public static int a = Integer.valueOf(1);
    static {
        System.out.println("SubTest2 is initialized.");
    }
}

以上问题的答案分别是:

  1. Yes
  2. Yes
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值