类的生命周期可以划分为 7 个阶段
其中,第 1~5 阶段,即加载、验证、准备、解析、初始化,统称为「类加载」,如下图所示。
加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class
对象,这个 Class
对象就是这个类各种数据的访问入口。
该过程可以总结为「JVM 加载 Class
字节码文件到内存中,并在方法区创建对应的 Class
对象」。
当 JVM 加载完 Class
字节码文件,并在方法区创建对应的 Class
对象之后,JVM 便会启动对该字节码流的校验,只有符合 JVM 字节码规范的文件才能被 JVM 正确执行。
这个校验过程,大致可以分为下面几个类型
0x cafe babe
int
类型的参数,但是使用它的时候却传入了一个 String
类型的参数。准备阶段中,JVM 将为类变量分配内存并初始化。
准备阶段,有两个关键点需要注意
内存分配的对象
Java 中的变量有「类变量」和「类成员变量」两种类型。「类变量」指的是被 static
修饰的变量,而其他所有类型的变量都属于「类成员变量」。 在准备阶段,JVM 只会为「类变量」分配内存,而不会为「类成员变量」分配内存。「类成员变量」的内存分配需要等到初始化阶段才开始。
public static int factor = 3; public String website = "www.google.com"; 复制代码
如上代码,在准备阶段,只会为 factor
变量分配内存,而不会为 website
变量分配内存。
初始化的类型
在准备阶段,JVM 会为「类变量」分配内存并为其初始化。这里的「初始化」指的是为变量赋予 Java 语言中该数据类型的零值,而不是用户代码里初始化的值。
public static int sector = 3; 复制代码
如上代码,在准备阶段后, sector
的值将是 0,而不是 3。
如果一个变量是常量(被 static final
修饰)的话,那么在准备阶段,变量便会被赋予用户希望的值。 final
关键字用在变量上,表示该变量不可变,一旦赋值就无法改变。所以,在准备阶段中,对类变量初始化赋值时,会直接赋予用户希望的值。
public static final int number = 3; 复制代码
如上代码,在准备阶段后, number
的值将是 3,而不是 0。
解析过程中,JVM 针对「类或接口」、「字段」、「类方法」、「接口方法」、「方法类型」、「方法句柄」、「调用点限定符」这 7 类引用进行解析。解析过程的主要任务是将其在常量池中的符号引用,替换成其在内存中的直接引用。
到了初始化阶段,用户定义的 Java 程序代码才真正开始执行。在这个阶段,JVM 会根据语句执行顺序对类对象进行初始化。
一般来说,当 JVM 遇到下面 5 种情况的时候会触发初始化
new
、 getstatic
、 putstatic
、 invokestatic
这 4 条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
new
关键字实例化对象的时候、读取或设置一个类的静态字段(被 final
修饰、已在编译器把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。java.lang.reflect
包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。main()
方法的那个类),虚拟机会先初始化这个主类。java.lang.invoke.MethodHandle
实例最后的解析结果是 REF_getstatic
、 REF_putstatic
、 REF_invokeStatic
的方法句柄,并且这个方法句柄所对应的类没有进行初始化时,则需要先出触发其初始化。当 JVM 完成初始化阶段之后,JVM 便开始从入口方法开始执行用户的程序代码。
当用户程序代码执行完毕后,JVM 便开始销毁创建的 Class
对象,最后负责运行的 JVM 也退出内存。
下面,将通过几个案例,对类加载的 5 个阶段加深理解。
public class Book { public static void main(String[] args) { System.out.println("Hello Liu Baoshuai"); } Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } { System.out.println("书的普通代码块"); } int price = 110; static{ System.out.println("书的静态代码块"); } static int amount = 112; } 复制代码
运行上述代码,输出信息如下。
书的静态代码块 Hello Liu Baoshuai 复制代码
下面对输出结果进行分析。
根据「类的生命周期和加载过程 / 5.初始化」章节中提到的「当虚拟机启动时,用户需要指定一个要执行的主类(包含 main()
方法的那个类),虚拟机会先初始化这个主类」可知,我们将会进行类的初始化。
Java 源代码中有构造方法这个概念。但编译为字节码后,是没有构造方法这个概念的,只有「类初始化方法」和「对象初始化方法」。
上面的例子中,其类初始化方法如下。
static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
上面的例子中,其对象初始化方法如下。
{ System.out.println("书的普通代码块"); } int price = 110; //注意,构造函数的代码一定是被放在最后的 Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } 复制代码
结合「类初始化方法」和「对象初始化方法」的分析,再回过头看上述例子,就不难得出结论了。
main
方法中,并没有实例化对象,所以只执行「类初始化方法」,如下所示。因此,会输出 书的静态代码块
。static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
main()
方法。因此,会输出 Hello Liu Baoshuai
。案例引申
下面,对上述测试案例进一步引申,修改 main()
方法,代码如下所示。
public class Book { public static void main(String[] args) { System.out.println("Hello Liu Baoshuai" + new Book().price); } Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } { System.out.println("书的普通代码块"); } int price = 110; static{ System.out.println("书的静态代码块"); } static int amount = 112; } 复制代码
运行上述代码,输出信息如下。
书的静态代码块 书的普通代码块 书的构造方法 price=110,amount=112 Hello Liu Baoshuai110 复制代码
下面对输出结果进行分析。
书的静态代码块
static { System.out.println("书的静态代码块"); } static int amount = 112; 复制代码
main()
方法。遇到了 new Book()
语句,所以触发执行「对象初始化方法」,如下所示。// part 1 { System.out.println("书的普通代码块"); } // part 2 int price = 110; // part 3 Book() { System.out.println("书的构造方法"); System.out.println("price=" + price +",amount=" + amount); } 复制代码
part 1
和 part 2
的先后顺序,是根据它们在代码中出现的顺序决定的。 part 3
部分是构造函数部分,这部分永远是出现最后的,和它在代码中的顺序无关。在代码中, part 3
部分虽然出现在 part 1
和 part 2
的前面,但在「对象初始化方法」中,它永远是出现在最后的。part 2
出现在 part 3
前面,所以输出 price
的值是 110,而不是 0。class Grandpa { static { System.out.println("爷爷在静态代码块"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public static int factor = 25; public Father() { System.out.println("我是爸爸~"); } } class Son extends Father { static { System.out.println("儿子在静态代码块"); } public Son() { System.out.println("我是儿子~"); } } public class InitializationDemo { public static void main(String[] args) { System.out.println("爸爸的岁数:" + Son.factor); //入口 } } 复制代码
运行上述代码,输出信息如下。
爷爷在静态代码块 爸爸在静态代码块 爸爸的岁数:25 复制代码
下面对输出结果进行分析。
main
方法中,并没有实例化对象,所以只执行「类初始化方法」,不会执行「对象初始化方法」。Son
初始化时,会先进行父类 Father
的初始化。同理,进行 Father
初始化时,会先进行父类 Grandpa
的初始化。所以,程序会输出如下信息。爷爷在静态代码块 爸爸在静态代码块 复制代码
main()
方法中的 System.out.println
语句,程序会输出 爸爸的岁数:25
。也许会有人问为什么没有输出「儿子在静态代码块」这个字符串?这是因为对于静态字段,只有直接定义这个字段的类才会被初始化,才会执行该类的「类初始化方法」。因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
class Grandpa { static { System.out.println("爷爷在静态代码块"); } public Grandpa() { System.out.println("我是爷爷~"); } } class Father extends Grandpa { static { System.out.println("爸爸在静态代码块"); } public Father() { System.out.println("我是爸爸~"); } } class Son extends Father { static { System.out.println("儿子在静态代码块"); } public Son() { System.out.println("我是儿子~"); } } public class InitializationDemo { public static void main(String[] args) { new Son(); //入口