加载过程
类的加载过程大致分为三种:加载阶段、连接阶段、初始化阶段
如图:
加载阶段
将Class文件的二进制数据读取到内存之中,然后将该字节流所代表的静态存储结构转换为方法区中运行时的数据结构,并且在对内存中生产一个该类的java.lang.class对象,作为访问方法区数据结构的入口。
连接阶段
将class通过一系列验证,准备,在解析最终形成一个可以被JVM认可的文件。
连接阶段细分为三个阶段:
- 验证 : 验证主要的工作就是为了保证class是一个符合JVM规则的class
- 准备:给与当前class中的数据类型赋予初始值(引用类型为null)
- 解析:将class中的符号引用直接解析为直接引用。(如“com.ch.demo.Test”)
初始化阶段
初始化阶段的工作就是将数据类型被赋予正确的值
如(int i=1,在准备阶段是i=0,在初始化阶段是i=1)
了解类是属于主动使用还是被动使用
JVM规定了以下几种方式是属于主动使用,其他的都是属于被动使用。
- 访问类中的静态变量
- 访问类中的静态方法
- 初始子类会导致父类初始化,当然如果只是初始化子类中的父类,那么父类会被初始化,子类不会初始化
- 通过反射方法
- 通过New 关键字来初始化对象
- 启动类,包含Main方法的类会导致初始化
特例:
- 数组的创建不会导致初始化: int[] a=new int[10],这种情况在会直接在JVM内存中开辟一块内存空间。
- 访问类中常量,常量在被声明的时候已经在方法区(元空间)中被声明了一块内存空间,访问常量实际上是访问内存中的数据
详细分析连接的三个阶段
验证
(1)文件格式效验:效验文件是否合法的前置关卡
- 验证文件格式,通过效验class文件二进制头部的魔术因子是否为0xCAFEBABE
- 验证主次版本号,JDK的主次版本号编译的calss文件是不一样的,低版本的不能兼容高版本class。
- 验证class文件是否缺少,class文件的Md5指纹是否一致(每个类在编译阶段会经过MD5算法,都会将结果一并附加给Class字节流作为字节流的一部分)
- 常量池不支持的类型
- 常量池中的不存在常量
- 其他
(2) 元数据的验证:效验文件内容是否符合JVM语义。
- 检查这个文件是否存在父类,实现某一个接口,这些父类的方法,接口方法是否存在,是否合法。
- 检查该类是继承了被final 修饰的类,JVM规定被final修饰的类不允许被继承,方法且不能被重载。
- 检查该类是否为抽象类,如果不是则判断该类是否继承父类的抽象的方法或者接口中的所有方法。
- 检查方法重载的合法性,如果参数相同,方法名相同,返回类型不同这样也是不被允许的。
- 其他语义效验
(3)字节码效验:该部分主要是对程序的程序指令分析,比如程序的循环、分支、条件判断
- 保证当前线程的程序计数器中的指令不会跳转其他线程的程序计数器中去。
- 保证类型的转换保持一致,比如用A声明的引用不能用B类型强制转换。
- 保证任意时刻,虚拟机栈中都操作数栈类型与指令都能正确的执行,比如在压栈的时候传入的是A类型的引用,当在使用的时候却是B类型的引用
- 其他验证
(4) 符号引用验证:保证符号引用能够正确的变成直接引用。
- 根据符号内的全限定的包名是否能够找到该文件
- 该文件内的方法,属性,类,是否对当前类可见。如:私有方法不对外可见
符号验证通过会出现NoSuchFieldError,NoSuchMethdError,这些在反射时也常见。
准备
在准备阶段中会为class文件中的属性字段赋予初始值(不是最终结果),各数据类型可以参考下表:
解析
解析阶段是将所有的类,接口,字段,方法的符号引用都变为直接引用的过程。
解析过程的主要针对类接口,字段,类方法,接口方法这个四类进行的,对应常量池中的分别是:Constant_Class_info、Constant_Fieldref_info、Constant_Methoderf_info和Constant_InterfaceMethoderd_info这四种常量。
(1)类接口
- 数组类型不需要经过加载而是直接在开辟一块内存空间并提供引用,而对象类的加载,在加载过程中需要经过类加载成功后,才可以被使用
- 在类接口的解析完成之后还需要进行对符号引用的验证
(2)字段
- 字段解析就是在解析访问类或者接口的字段,该字段是否存在,不存在则抛出异常。
- 当类本身就有个字段属性,那么则直接返回字段引用。如果该字段为类对象属性,当然也要提前加载该类对象。
- 当类中未找到字段属性,那么自下而上依次查找父类或者接口中是否被定义该字段,如果都未定义,则抛出NotSuchFieldError异常。
- 同样在查找的过程中发现有对象属性也需要提前加载。
(3)类方法
- 类方法可以在本类或者子类调用,而接口方法必须子类实现接口方法。
- 类方法的查找也是向上查找,当在本类或者父类找到该方法则直接返回方法引用。
如果找到的方法是一个抽象方法则抛出AbstractMethodError
如果一直找到父类还没有找到则抛出NoSuchMethodError,
- 当类方法表的Class_index表示为 接口而不是类,则直接返回错误
(4)接口方法
- 当接口方法表中发现class_index不是接口而是类则直接返回错误。
- 查找方法过程也是向上查找未找到就抛出 NosuchMethodError
初始化阶段底层执行过程
在初始化阶段的一个主要的过程就是完成<clinit:class initiaiize>,
<clinit> 方法在编译阶段生成的。
<clinit>内包含对变量的赋值动作以及对静态代码块的执行代码。
编译器收集的顺序是有执行语句在源文件出现的顺序。<clintit >是保证顺序性的。
<clinit>与类的构造方法是不同概念。<clinit>不会显示去调用父类的构造方法。
它会保证父类的<clinit>会被最先执行。