当程序主动使用某个类时,如果该类还未被加载到内存中,则JVM会通过加载、连接、初始化3个步骤来对该类进行初始化。
在聊 Java 类加载机制之前,需要先了解一下 Java 字节码,因为它和类加载机制息息相关。
计算机只认识 0 和 1,所以任何语言编写的程序都需要编译成机器码才能被计算机理解,然后执行,Java 也不例外。
Java 在诞生的时候喊出了一个非常牛逼的口号:“Write Once, Run Anywhere”,为了达成这个目的,Sun 公司发布了许多可以在不同平台(Windows、Linux)上运行的 Java 虚拟机(JVM)——负责载入和执行 Java 编译后的字节码。
我们借助一段简单的代码来看一看,源码如下:
package org.blackist.jvm;public class JvmDemo { public static void main(String[] args) { System.out.println("董亮亮的开发笔记"); }}
代码编译过后,通过十六进制工具 xxd JvmDemo.class命令查看这个字节码文件:
00000000: cafe babe 0000 0034 0022 0a00 0600 1409 .......4."......00000010: 0015 0016 0800 170a 0018 0019 0700 1a07 ................00000020: 001b 0100 063c 696e 6974 3e01 0003 2829 .....<init>...()00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN00000040: 756d 6265 7254 6162 6c65 0100 124c 6f63 umberTable...Loc00000050: 616c 5661 7269 6162 6c65 5461 626c 6501 alVariableTable.
这段字节码中的 cafe babe
被称为“魔数”,是 JVM 识别 .class 文件的标志。文件格式的定制者可以自由选择魔数值(只要没用过),比如说 .png 文件的魔数是 89504e47
。
JVM结束生命周期的几种情况:
Java 的类加载过程可以分为 5 个阶段:载入、验证、准备、解析和初始化。这 5 个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。
查找并加载类的二进制数据。
将类的.class文件中的二进制数据读入到内存,将其放在运行时数据区的方法区内,然后在堆去创建java.lang.Class对象,用来封装类在方法区内的数据结构。
确保被加载的类的正确性,符合JVM字节码规范,该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。
cafe bene
开头)。JVM 会在该阶段对类变量(也称为静态变量, static
关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等)。
public String blackist = "Blackist";public static String note = "Note";public static final String bnote = "Note-of-Blackist";
blackist不会被分配内存,而 note会;但 bnote的初始值不是“王二”而是 null
。
需要注意的是, staticfinal
修饰的变量被称作为常量,和类变量不同。常量一旦赋值就不会改变了,所以 bnote 在准备阶段的值为“沉默王二”而不是 null
。
该阶段将常量池中的符号引用转化为直接引用。
符号引用以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。
在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 org.blackist.Quiz
类引用了 org.blackist.Bnote
类,编译时 Quiz类并不知道 Bnote类的实际内存地址,因此只能使用符号 org.blackist.Bnote
。
直接引用通过对符号引用进行解析,找到引用的实际内存地址。
类变量已经被赋过默认初始值,而在初始化阶段为类的静态变量赋予正确的初始值。
public class Test { // 准备阶段默认值为0,初始化阶段赋值3 private static int foo = 3; // 也可写为 peivate static int foo; static { foo = 3; } // 静态代码块从上到下顺序执行,foo最终等于4 static { foo = 4; }}
换句话说,初始化阶段是执行类构造器方法的过程。
class Parent {}class Child extends Parent { public static int a = 3;}// 初始化子类,对父类进行了主动使用Child.a = 4;
所有JVM实现必须在每个类或接口被Java程序 首次主动使用 时才初始化。
除了主动使用以外的使用,都不会导致类的初始化。
如下程序:
输出:
10
准备阶段:singleton=null, counter1=0, counter2=0
主动调用:Singleton.getInstance()触发主动调用,进行初始化
初始化阶段:singleton=new Singleton()调用构造方法[counter1=1,counter2=1],counter1不变,counter2=0