类加载过程
一个类从编写完成后,编译为字节码之后,它要装载进内存有七个阶段:
加载 => (验证-> 准备-> 解析)=> 初始化=> 使用=> 卸载
括号中的三个步骤可以整合成为 “连接”步骤。其中的步骤并不是一个阶段结束,一个阶段才开始的。只是说他们的开始阶段基本遵循此顺序(解析阶段更是可能在使用的时候才发生,目的是配合动态绑定),这些阶段都是互相交叉的混合式进行的,通常会在一个阶段执行过程中调用或激活另一个阶段。
1.加载
”加载“的过程是”类加载“过程的一个阶段,在家在阶段,虚拟机需要完成以下三件事:
1)通过一个类的全限定名来过去定义此类的二进制流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时的数据结构
3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区原来的类里面数据的入口
而JVM并没有详细规定这三个步骤的详细实践,例如第一条的二进制流的来源就没有确定,因此诞生了很多我们熟知的引入类的方法:
-
-
- 从ZIP包获取,对应的技术是的JAR、EAR、WAR包引入的基础,各种框架引入外部包的技术基石
- 从网络中获取,对应的是Applet(一种类似于JavaScript应用的技术,技术上完全不相似,sun公司以前希望其作为浏览器上程序的运行环境)
- 运行时计算生成,对应的是动态代理技术
- 由其他文件生成,对应JSP应用里的各种应用
-
需要特别说明的是,加载阶段还没有结束的时候,连接阶段就已经开始了
2.验证
验证时连接的第一步,是为了确保Class文件的字节流中的包含信息符合当前虚拟机的要求,并不会对虚拟机危险。
JVM标准并没有对此提出标准化步骤,但一般的JVM实现会有以下四个过程:
1)文件格式验证,判断是否符合格式规范
这个阶段会验证格式是否正确、是否符合版本要求、编码是否是UTF-8等
2)元数据验证,堆字节码进行语义分析
这个阶段验证是否有父类(除了Object之外的类都应该有父类)、是否继承了不允许继承的类(如被final修饰的类)、是否实现了父类或接口中的所有方法等
3)字节码数据验证,对数据流和控制流进行分析,是最复杂的一个阶段
这个阶段的实例有 类型是否匹配(比如声明一个int型变量却放入一个long)、跳转指令的正确跳转(保证循环分支的正确执行)、类型转换是否有效(子类对象赋值给父类可以反之不行)
4)符号引用验证
例如,符号引用通过字符串描述的全限定名是否能找到对应的类(JDBC用到很多),
还有一个很重要的就是类、字段、方法的访问性(privateprotectedpublicdefault)是否可以被当前类访问。
3.准备
这一阶段正式为类变量(satic修饰的部分)分配内存并设置类变量初始值,一般为0值。
例如: public static int value = 1;
此时准备阶段会准备value在方法区中,值为0,在初始化阶段,才会将1赋值给value。但如果被final修饰,这一阶段就会给其赋值。
4.解析
将常量池中的符号引用替换为直接引用的过程。
5.初始化
将准备阶段的值改变为程序员指定的值。
初始化阶段是执行类构造器<clinit>()方法的过程,这个方法是:
1)编译器自动收集类中所有类变量的赋值动作和静态语句块合并产生的
2)而且是先变量赋值,再静态语句块的顺序(不确定)
3)并且在子类的<clinit>()方法执行前,父类其方法一定已经执行完毕了
下面看一段程序:
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.println(Sub.B); }
很明显,应该输出2,因为先在Parent中A赋值为1,又在static中变为了2,然后才执行子类中的赋值。
但是我对上文中提到的2)顺序表示怀疑,因为以下代码运行结果为1。
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.println(Sub.B); }
因此,《深入理解Java虚拟机》中提到的<clinit>()的静态代码块与变量赋值的顺序在此不作确定,个人在win8-64位机器上测试(HotSpot JVM)为与代码顺序有关,原书作者认为无关。