一、类的加载过程
类从加载到内存中开始,到卸载出内存位置,为类的生命周期。
包括加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initiazation)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、连接统称为连接(linking)。
- 其中加载、验证、准备、初始化和卸载这5个阶段的顺序是一定的;类的加载过程必须按照这个顺序按部就班的开始,而解析阶段不一定;
- 解析阶段在某些情况下,可以在初始化之后解析,支持java语言的运行时绑定(也就是动态绑定或晚期绑定)
1、Loading加载
- (1)同过全限定名获取定义此类的二进制流;
- (2)将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内;
- (3)然后在内存中创建一个java.lang.Class对象(规范并未说明Class对象位于哪里,HotSpot虚拟机将其放在方法区中)用来封装类在方法区的数据结构,并作为方法区这个类的各种数据的访问入口;
- (4)加载.class的方式:
- 本地系统直接加载;
- 从zip包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础;常见的应用程序在服务器上部署就是从JAR包中读取.class文件;
- 从网络中获取,这种场景最典型的就是Applet(小程序);
- 运行时计算生成,这种场景使用的最多的就是动态代理,在java.lang.reflect.Proxy,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流;;
- 从专有数据库中提取.class文件;例如:中间件服务器(SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发;
- 有其他文件生成,如:JSP应用,即有JSP文件生成对应的Class类
- 其他
2、Verification验证(Linking连接的第一阶段)
验证是连接阶段的第一步,包含:文件格式验证、元数据验证、字节码验证、符号引用验证。
验证阶段非常总要,但不一定必要,可以在实施阶段使用-Xverify:none来关闭大部分的验证措施。
(1)文件格式验证
- 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 通过文件格式验证后,字节流会进入内存的方法区中进行存储;
- 其余3个验证阶段都是基于方法区的存储结构进行的,不会再直接操作字节流;
- 验证点包括:
- 是否以魔数0xCAFEBABE开头;
- 主、次版本号是否在当前虚拟机的处理范围内;
- 常量池中的常量类型是否有不被指出的常量类(检查常量tag标志);
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据;
- Class文件中各个部分及文件本身是否有被删除的或附加的其他信息;
- …
(2)元数据验证
对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息。
验证点包括:
- 这个类是否有父类(除了java.lang.Object之外,所有的类都应该有父类);
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类);
- 如果这个类不是抽象类,是否继承了其父类或接口中要求实现的所有方法;
- 类中的字段、方法是否与父类产生矛盾(例如:覆盖了父类的final方法、不符合规则的方法重载,例如方法参数都一样,返回值类型却不同);
(3)字节码验证
- 通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的;
- 对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件;
(4)符号引用验证
对自身以外(常量池中的各种符号引用)的信息进行匹配行校验。
3、Preparation准备(Linking连接的第二阶段)
- 为类的静态变量在方法区中分配内存,设定初始值;
- 不包括实例变量,实例变量是随着对象实例化之后和对象一起分配在java堆中;
- 如:
public static int value=3;
在准备阶段过后初始值为0,而不是3。只有在初始化之后该内存中的值才会变成3.
java中所有基本数据类型的零值:
数据类型 | 零值 |
---|---|
int | 0 |
long | 0L |
short | (short) 0 |
char | ‘\u0000’ |
byte | (byte) 0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
特殊情况:
public static final int value = 3;
编译时javac会为value生成ConstantValue属性,在准备阶段就会根据ConstantValue属性直接将value赋值为3;
4、Resolution解析(Linking连接的第三阶段)
- 将常量池中的符号引用替换为直接引用的过程;
- 顺序不定,有可能在初始化之后解析,根据需要判断是在类加载器加载时解析还是在使用前解析;
- 解析动作主要针对类和接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符;
示例代码
public class JVMClassResolutionTest {
interface Interface0{
int A=0;
}
interface Interface1 extends Interface0{
int A=1;
}
interface Interface2{
int A=2;
}
static class Parent implements Interface1{
public static int A=3;
}
static class Son extends Parent implements Interface2{
//public static int A=4;
}
public static void main(String[] args) {
System.out.println(Son.A);
}
}
报错:The field Son.A is ambiguous
5、Initialization初始化
初始化阶段是执行类构造器<clinit>()
方法的过程。为类的静态变量赋予真正的初始值。
-
<clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,收集顺序根据源文件顺序决定。静态语句块只能访问定义在静态语句块之前的变量,如:-
示例1
public class Test16 { static { i =1; } static int i =0; }
解析:代码正常编译通过,定义在静态语句块之后的变量,在静态语句中可以赋值;
-
示例二
public class Test16 { static { i =1; System.out.println("i===="+i); //代码编译报错Cannot reference a field before it is defined(非法向前引用) } static int i =0; }
解析:定义在静态语句块之后的变量,在静态语句中可以赋值,不可以访问;
- 示例三
public class Test16 { static { i =1; } static int i =0; static { System.out.println("i===="+i); } }
解析:定义在静态语句块之后的变量,在静态语句中可以赋值,在其之后可以访问;
-
-
类的加载顺序。示例如下:
public class ClassLoaderTest {
public static void main(String[] args) {
son sons=new son();
}
}
class parent{
private static int a=1;
private static int b;
private int c=initc();
static {