首先上图
如图所示,Java类加载机制的六个阶段
Java代码编译完成后会生成对应的class文件,接着我们运行java命令的时候,其实是启动了JVM虚拟机执行class字节码文件的内容。大致分为六个阶段:加载、验证、准备、解析、初始化使用、卸载。
1.加载
加载阶段是类加载过程的第一个阶段。此阶段JVM将字节码从各个位置(网络、磁盘等地方)转化为二进制字节流加载到内存中,接着会为这个类在JVM的方法区(元空间)创建一个对应的Class对象,这个Class对象就是这个类各种数据的请求入口。
描述:把代码数据加载到内存中。
2.验证
当JVM加载完Class字节码文件并在方法区创建对应的Class对象后,JVM就会启动对该字节码流的校验,只有符合JVM字节码规范的文件才能被JVM正确执行。校验如下:
JVM校验规范:JVM会对字节流进行文件格式校验,判断其是否符合JVM规范,是否能被当前版本的虚拟机处理。
代码逻辑校验:JVM会对代码组成的数据流和控制流进行校验,确保JVM运行该字节码文件后不会出现致命错误。
3.准备
当完成字节码文件校验后,JVM开始为类变量分配内存并初始化。两个关键点:内存分配的对象以及初始化类型。
内存分配对象:Java中的变量有[类变量]和[类成员变量]两种类型,[类变量]指的是被static修饰的变量,而其他所有类型的变量属于[类成员变量]。在准备阶段,JVM只会为[类变量]分配内存,而不会为[类成员变量]分配内存。[类成员变量]的内存分配需要等到初始化阶段才开始。
如下代码,在准备阶段只会为age属性分配内存,而不会为website属性分配内存。
public static int age = 3;
public String website = "www.beijingdesigner.com"
例如下面的代码在准备阶段之后,age的值将会是0,而不是3;
public static int age = 3;
但如果一个变量是常量(被static final修饰)的话,在准备阶段属性便会被赋予用户希望的值。如下代码,number的值是3,而不是0.
public static final int number = 3;
结论:在准备阶段,类变量会被分配内存,类成员变量不会被分配内存,当类属性被final修饰,即不可改变被赋予最终值,否则赋予初始值。
4.解析
当通过准备阶段之后,JVM针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类引用进行解析。这个阶段的主要任务是将其在常量池中的符号引用替换成其在内存中的直接引用。
初始化使用
到了初始化阶段Java程序代码才真正开始执行。在这个阶段,JVM会根据语句执行顺序对类对象进行初始化,一般来说当JVM遇到下面情况的时候回触发初始化:
遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有初始化,则需要先触发其初始化。生成这4条指令的最长见的Java场景是:1.使用new关键字实例化对象 2.读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)3.调用一个类的静态方法时。
6.卸载
当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象,最后负责运行的JVM也退出内存。
实战分析
关于类加载机制的几个demo
public class GrandFather{
static{
System.out.println("GrandFather在静态代码块");
}
}
public class Father extends GrandFather{
static{
System.out.println("Father在静态代码块");
}
public static int age = 25;
public Father(){
System.out.println("我是Father");
}
}
public class Son extends Father{
static{
System.out.println("Son在静态代码块");
}
public Son(){
System.out.println("我是Son~");
}
}
测试类:
public class FGSTest{
public static void main(String[] args){
System.out.println("Father的岁数:"+Son.age);
}
}
最终输出结果:
GrandFather在静态代码块
Father在静态代码块
Father的岁数:25
为什么没有输出[Son在静态代码块]呢?
这是因为对于静态字段,只有直接定义这个字段的类才会被初始化(执行静态代码块),因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
分析
执行main方法,使用标准化输出Son类中的age类成员变量,但是Son类中并没有定义这个类成员变量。于是往父类去找,找到了对应的类变量,于是触发了Father的初始化。
但是根据我们上面说的初始化情况,我们需要初始化GrandFather类再初始化Father类。于是我们先初始化GrandFather类输出:[GrandFather在静态代码块],再初始化Father类输出:[Father在静态代码块]。
最后,所有父类都初始化完成后,Son类才能调用父类的静态变量,输出[Father的岁数:25]。