类的加载过程
1. 加载
-
通过一个类的全限定名来获取定义此类的二级制字节流。
-
将这个字节流所代表的静态存储结构转化为方法区的运行时的数据结构。
-
在内存中生成一个代表此类的java.lang.Class对象,作为方法区这个类的各种数据的访问人口 。
注意:数组类本身不通过类加载器创建,而通过Java虚拟机直接创建。
2. 验证
目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
-
文件格式验证
验证字节流是否符合Class文件格式规范,并被当前虚拟机所处理。
例如:是否以0xCAFEBABY开头、指向常量的各种索引值是否指向了不存在的常量或不符合类型的常量等等。
目的是确保输入的字节流能正确的解析并存储于方法区之内,格式上描述符合一个Java类型信息的要求。
该阶段的验证是基于二进制字节流的,通过该验证后字节流才进入方法区中存储。故之后的三个验证是基于方法区的存储结构进行的。
-
元数据验证
对字节码描述的信息进行语义分析,以保证描述的信息符合Java语言规范的要求。
例如:是否有父类、其父类是否继承了不允许继承的类、类中的字段、方法是否与父类产生矛盾等等。
-
字节码验证
通过数据流和控制流的分析,确定程序语法是否是合法的、符合逻辑的。主要对类的方法进行校验分析。
JDK 1.6后可以通过检查StackMapTable属性中的记录是否合法即可,节省时间。但该属性存在错误或被篡改的可能,以欺骗虚拟机。
-
符号引用验证
发生在将符号引用转化为直接引用的时候,一般在解析阶段发生。
对类自身以外的信息进行匹配性校验,目的是确保解析动作可以正确执行。
例如:符号引用中的类、字段、方法的访问性、是否通过符号引用找到全限定名的对应类等等。
对于虚拟机的类加载机制而言,验证阶段非常重要,但并非一定必要。若所运行的全部代码都已经反复使用和验证过,可以关闭大部分的类验证以缩短虚拟机类加载的时间。
3. 准备
正式为类变量分配内存并设置初始值。
-
进行内存分配的仅包括类变量(被static修饰的变量),但不包括实例变量(它将咋对象实例化时随对象一起分配在Java堆中)。
-
其初始值通常情况下是零值,并非任何方法的指定赋值。因为此时并未开始执行任何Java方法,相关的具体赋值将在类构造器()中进行。
例如:public static int value = 123;准备阶段过后其值为0,而非123。
-
若类字段属性表中存在ConstantValue属性,将在准备阶段初始化其指定的值。
例如: public ststic final int value = 123; 将会在准备阶段给value赋值为123。
4. 解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述引用的目标,使用时可以无歧义的定位到目标即可。其与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到虚拟机内存中。
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能够间接定位到目标的句柄。其与虚拟机实现的内存布局相关,同一符号引用在不同虚拟机实例上翻译出的直接引用一般不会相同。若有直接引用,则目标必定存与内存中。
- 类或接口的解析
- 字段的解析
- 类方法的解析
- 接口方法的解析
5. 初始化
执行类构造器()的方法.
- ()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句联合产生。
- 与类的构造函数不同,不需要显示调用父类构造器,虚拟机确保了父类的()方法已经在子类构造器方法执行之前完成。
- 父类中定义的静态语句块要优于子类的变量赋值操作。
- 该方法对于类或接口来说不烦必需。
- 虚拟机会确保一个类的()方法在多线程环境中被正确加锁和同步。同一类加载器下,一个类型只会初始化一次。一个线程执行该方法时,其他线程会阻塞。
类与类加载器
对于任意一个类,都需要有加载它的类加载器和它本身一同确立其在Java虚拟机中的唯一性,每一个加载器都有独立的命名空间。不同类加载器加载的源于同一Class文件的类必定不同。
类加载器的类型
从Java虚拟机角度
- 启动类加载器:使用c++语言实现,是虚拟机的一部分。
- 其他的类加载器:由Java语言实现,独立于虚拟机外,并且全部继承自抽象类java.lang.ClassLoader。
从开发人员角度
- 启动类加载器
- 扩展类加载器
- 应用类加载器
双亲委派模型
要求除顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。