上篇文章提到过,类加载一共七步骤:加载、验证、准备、解析、初始化、使用、卸载。现在讲解前五步骤。
一、加载
在类加载阶段,虚拟机需要完成以下三件事:
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区对这个类的各种数据的访问入口
总结:查找并加载二进制字节码到方法区,然后在内存中实例化一个java.lang.Class类的对象
二、验证
1、文件格式验证
1)是否以魔数0xCAFEBABE开头
2)主次版本号是否在当前虚拟机处理范围之内
3)常量池的常量中是否有不被支持的常量类型
4)是否有指向不存在的常量或不符合类型的常量
5)CONSTANT_Utf8_info型的长两种是否有不符合UTF8编码的数据
6)Class文件中各个部分及文件本身是否又被删除的或附加的其他信息
...
该阶段的主要目的是保证输入的字节流能正确的解析并存储于方法区之内
2、元数据验证
1)这个类是否有父类(除了java.lang.Object之外,其他所有的类都应该有父类)
2)这个类的父类是否继承了不允许被继承的类(被final修饰的类)
3)如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
4)类的字段,方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,比如方法参数都一样,但返回值却不同。)
...
该阶段主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
3、字节码验证
1)保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中
2)保证跳转指令不会跳转到方法体以外的字节码指令上
...
该阶段主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
4、符号引用验证
1)符号引用中通过字符串描述的全限定名是否能找到对应的类
2)在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
3)符号引用中的类、字段、方法的访问性是否可以被当前类访问
...
该阶段的目的是确保解析动作能正常执行,否则将会抛出一个java.lang.IncompatibleClasschangeError异常的子类
验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
三、准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。
1)这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
2)这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显示的赋予的值。
假设一个类变量的定义为:
public static int value = 3;
那么变量value在准备阶段过后的初始值为0,而不是3,因为这时候尚未开始执行任何Java方法,而把value赋值为3的putstatic指令是在程序编译后,存放于类构造器<clinit>()方法之中的,所以把value赋值为3的动作将在初始化阶段才会执行。
3)如果类字段的字段属性表中存在ConstantValue属性,即同时被static 和 final修饰,那么在准备阶段变量value就会会初始化为ConstantValue属性所指定的值。
假设上面的变量value被定为:
public static final int value = 3;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。
总结:为类的静态变量分配内存,并将其初始化为默认值
四、解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
总结:把类中的符号引用转换为直接引用
五、初始化
初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
①声明类变量是指定初始值
②使用静态代码块为类变量指定初始值
初始化也是执行类构造器<clinit>()方法的过程。
1)<clinit>()方法是由编译器自动收集类种所有变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块可以赋值,但是不能访问,如下:
package com.designmodel.javap;
/**
* 测试
*
* @author 15620646321@163.com
* @date 2017年5月9日
*/
public class TestCLInit {
static {
i = 0;
//Cannot reference a field before it is defined
// System.out.println(i);
}
static int i = 1;
}
2)<clinit>()方法与类的构造函数不同,他不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
3)由于父类的<clinit>()方法优先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作,如下
package com.designmodel.javap;
/**
* 测试
*
* @author 15620646321@163.com
* @date 2017年5月9日
*/
public class TestCLInit2 {
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.print(Sub.B);
}
}
输出结果:
2
package com.designmodel.javap;
/**
* 测试
*
* @author 15620646321@163.com
* @date 2017年5月9日
*/
public class TestCLInit2 {
static class Parent {
static {
A = 2;
}
public static int A = 1;
}
static class Sub extends Parent {
public static int B = A;
}
public static void main(String[] args) {
System.out.print(Sub.B);
}
}
输出结果:
1
从结果发现,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。
若有兴趣,欢迎来加入群,【Java初学者学习交流群】:458430385,此群有Java开发人员、UI设计人员和前端工程师。有问必答,共同探讨学习,一起进步!
欢迎关注我的微信公众号【Java码农社区】,会定时推送各种干货: