- 接下来,我们会详细了解Java虚拟中类加载的全过程。
- 即加载、验证、准备、解析和初始化这五个阶段所执行的具体动作。
加载
- 在加载阶段下,Java虚拟机需要完成三件事
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问的入口。
- *对这三点的要求不是很具体,所以虚拟机实现的灵活度很大。
- 加载阶段可以使用虚拟机里内置的启动类加载器来完成,也可以由用户自定义的类加载器去完成。
- 开发人员可以通过定义自己的类加载器去控制字节流的获取方式。
- 对于数组类来说
- 数组类本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来。
- 加载结束之后,Java虚拟机外部的二进制字节流就按照虚拟机设定的格式存储在方法区之中。
- 同时会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为外部访问方法区中的类型数据的外部接口。
加载阶段与连接阶段的部分动作是交叉进行的。
验证
- 验证是连接的第一步,这一阶段的目的就是 确保Class文件的字节流包含的信息符合*的全部约束要求。保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
- 必要性:虽然Java语言是相对安全的,但是Class文件不一定只能由Java源码编译而来,它可以有很多途径产生。所以为了防止载入有错误或有恶意企图的字节码流而导致整个系统受攻击甚至崩溃,是由必要的措施。
- 验证阶段大致完成了下面四个阶段的检验动作。
- 文件格式验证
- 验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理。
- 目的:保证输入的字节流能够正确解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。
- 这阶段的验证是基于二进制字节流进行的,只有通过这阶段的验证之后,字节流才被允许进入Java虚拟机内存的方法去中进行存储。
- 元数据验证
- 对字节码描述的信息进行语义分析,以保证其描述的信息符合*的要求。
- 目的:对类的元数据信息进行语义校验
- 字节码验证
- 目的:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
- 在第二阶段对元数据信息中的数据类型进行校验完毕以后,这阶段就要对类的方法体(Class文件中的Code属性)进行校验分析,保证被验证类的方法在运行时不会做出危害虚拟机安全的行为。
- 当然就算通过了字节码验证,也不代表代码一定是安全的。不可能用程序来准确判定另一段程序是否存在bug。
- 符号引用验证
- 虚拟机将符号引用转化为直接引用的时候。 这个转化动作将发生在连接的第三阶段-解析中。
- 符号引用可以看成是对类自身以外的各类信息进行匹配性校验。就是查看该类是否缺少或者被禁止访问他依赖的某写外部类、方法、字段等资源。
- 目的:确保解析行为能够正常执行。
准备
- 准备阶段是正式为类中定义的变量(即静态变量、被static修饰的变量)分配内存并设置类变量初始值的阶段
- JDK 7 之后,类变量会随着Class对象一起存放在Java堆中
- 这时候进行内存分配的仅包括类变量,而不包括实例变量。实例变量会随着对象实例化时随着对象一起分配在Java堆中。
- 其次是这里的初始值指的是数据类型的零值。
- 把value赋值成123的putstatic指令是程序编译后,存放于类构造器()方法之中,所以要到类的初始化阶段才会执行。
public static int value = 123;
- 如果类的字段表中存在ConstantValue属性,那么在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值。
public static final int value = 123;
解析
- 解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程
- 上面代码中,Test类中引用了MyObject类和它的method方法。
- 在编译期,Java编译器将Test类中对MyObject类和method方法的引用转化为符号引用,并保存在字节码文件中。
- 在运行期,当编译器需要访问MyObject类和method方法时,它将符号引用转化为直接引用,即指向内存地址的指针。
public class Test {
public static void main(String[] args) {
MyObject obj = new MyObject();
obj.method();
}
}
class MyObject {
public void method() {
System.out.println("Hello, world!");
}
}
- *并没有规定解析阶段的具体发生时间,只要求在17个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析。
- 所以,虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的常量进行解析,还是等到一个符号引用要被它使用时前才去解析它。
能不能对同一个符号引用进行多次解析?
- 对同一个符号引用进行多次解析请求是很常见的事情。
- 虚拟机会对第一次解析的结果进行缓存,譬如在运行时直接引用常量池中的记录,并把常量标识为已解析状态,从而避免解析动作的重复进行。
- 一个符号引用如果已经被成功解析过,那么后续的引用解析请求就应当一直能够成功。反之。
具体引用的解析过程
- 解析动作主要是针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用。
1. 类或接口的解析
- 假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么虚拟机完成整个解析过程需要包括以下3个步骤。
2. 字段解析
- 在解析字段之前,首先会将字段所属的类或接口的符号引用进行解析。如果解析成功了,那把这个字段所属的类后接口用C表示,接下来按照以下步骤对C进行后续的字段搜索。
3. 方法解析
- 方法解析的第一个步骤与字段解析一样,也是需要先解析出方法所属的类或接口的引用。如果解析成功,依然用C表示这个类。
4. 接口方法解析
- 同理
初始化
-
类的初始化是类加载过程的最后一个步骤,之前的几个类加载动作中。
-
除了在加载阶段,用户应用程序可以通过自定义类加载器的方式参与外,其余的动作都是完全由Java虚拟机来主导。
-
直到初始化阶段,Java虚拟机才真正开始执行
类中的编写的Java程序代码
,将主导权移交给应用程序。 -
准备阶段
的时候,变量已经赋过一次系统要求的初始零值。而在初始化阶段
,会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。
- 静态语句块只能访问到定义在静态语句块前的变量。 定义在后面的变量,可以赋值但是不能访问。
public class ClintStaticTest {
static {
i = 0;
System.out.println(i);//提示非法前向引用
}
static int i = 1;
}
- ()方法保证在子类的该方法执行前,父类的()方法一定已经执行完毕,所以Java虚拟机中第一个被执行的一定是java.lang.Object
- 父类的静态语句块一定优先于子类的变量赋值操作。
public class Parent {
public static int A = 1;
static {
A = 2;
}
}
public class Sub extends Parent{
public static int B = A;
}
public static void main(String[] args) {
System.out.println(Sub.B);
}
- ()方法不是必须的,如果没有赋值操作,也没有静态语句块就不需要。
- 接口中可以赋值,所以也有该方法。 接口不需要先执行父接口的方法,因为只有父接口中定义的变量被使用时,父接口才会被初始化。
- 一个类的()方法需要在多线程环境中被正确地加锁同步。