加载
加载过程主要完成以下几件事情
- 类加载器根据类的全限定名获取类的二进制字节流
- 将二进制字节流表示的静态结构转换为方法区的运行时数据结构
- 在堆上面实例化一个该类的Class对象,作为方法区这个类的各种数据访问的入口
验证阶段
验证是连接阶段的第一步
验证阶段主要目的是确保Class文件的字节流中包含的信息符合java虚拟机规范,确保这些信息被加载
入内存不会危害到jvm自身的安全。主要包含以下几个步骤
-
文件格式验证
验证字节流是否符合Claa文件格式的规范要求,确保字节流所表示的静态结构能转换为运行时数据结 构,验证阶段只有此步骤是基于字节流的,后面的几个验证的步骤就是基于方法区的运行时数据结构 了 有以下内容 验证魔数 验证版本号 常量池是否有不支持的类型 CONSTANT_Utf8_info 有不符合Utf-8的编码的数据 等等等等
-
元数据验证
对字节码的信息进行语义分析 这个类是否有父类(除了java.lang.Object外应该全有父类) 这个类是否继承了不允许被继承的类(final类) 这个类是否实现了接口中应该实现的所有方法 等等
-
字节码验证
对class文件中的code属性进行验证,保障被验证类的方法在运行时不会做出危害虚拟机的行为。 例如字节码指令对应的操作数栈的数据类型是否一致,不会出现指令是将int类型存入局部变量表而 操作数栈却弹出long类型的数据存入变量表
-
符号引用验证
符号引用验证发生在解析阶段,及将符号引用转换为直接引用的过程。确保解析阶段的进行 主要有以下内容 根据符号引用描述的全限定类名是否能找到相应的类 在指定的类中是否有相应的字段或者方法 检查当前类对该字段和方法的访问权限
准备阶段
准备阶段是为类变量分配内存初始化为零值的过程
在准备阶段将为类中变量和类常量在方法区分配内存空间。并且如果是变量则初始化为零值,如果
是常量,则初始化为程序代码中指定的常量值。
值得注意的是,由于方法区相当于一个接口,只是逻辑上的区域。所以在JDK1.7以前类变量在永久
代,在JDK1.8以后类变量就随着Class实例化对象在堆上谜案。
tip:对于类变量的赋值语句,被整合到类构造器中(clinit), 在初始化阶段执行。
解析阶段
解析阶段将常量池中的符号引用转换为直接引用,再解析阶段除了堆符号引用的解析,最终逗得对解析结果进行权限的验证
符号引用;通过一组符号来描述所引用的目标,符号可以是任何形式的字面量只要能无歧义的定位
到目标即可
直接引用:直接指向目标的偏移量,指令或者是可以定位到目标的句柄
- 类和接口的解析
假设当前代码所处的类为C,要解析一个从未被解析过的符号引用N,这个N指向类或接口
如果N表示一个非数组类型
将代表N的全限定类名交给类的C类加载器加载,而在加载的过程中,元数据,字节码等阶段又可能
触发新的相关类的加载动作,只有这些连锁动作中任意动作抛出异常。则解析失败
如果表示一个数组类型例如 [Ljava/lang/Integer
首先按照第一步加载数组的类型,然后由JVM生成一个代表该数组维度和元素类型的对象
最后验证权限
最后确认类D是否对类C具有访问权限,如果没有,则抛出异常
-
字段的解析
首先根据字段表中的class_index加载该字段的所属类C 如果类C加载成功,则在C中茶盅是否有与该字段简单名称和描述符相等的字段,如果有返回 如果没有,从类C的父接口从上到下的递归寻找 如果还没有,且该C不是java.lang.Object,则在父类中从上到下的递归查找 最后如果找到,则检查访问权限 如果未找到,则抛出异常
-
类方法的解析
首先根据方法表中的class_index加载该方法所属类型C 加载成功后。因为这是一个类方法,所以先检查C是接口还是类,如果是接口抛出异常 通过后,则检查类C中是否有与该方法简单名称和符号引用都相同的方法,如果有则返回 否则,在父类中递归查找,找到则返回 否则,在类C实现的接口中查找,如果找到则说明这是一个抽象类直接抛出异常 否则,查找失败。 最后如果查找成功,则老规矩,进行权限验证
-
接口方法的解析
接口方法的解析和类方法的解析相类似 第一步,加载class_index指向的类型C 检查C是否是一个接口,如果不是,抛出异常 如果是的话,在接口中查找该方法简单名称和符号引用都相同的方法 如果未找到,在父接口中递归查找。 如果还是未找到,则失败抛出异常
tip:接口中的方法都是public static 的所以不用验证权限
初始化阶段
初始化阶段是类加载的最后一个阶段,该阶段对类中的变量及其他资源进行初始化
在准备阶段,已经对类变量进行一次赋零值的操作,但是在初始化阶段才是真正的按照从程序员的
编码对类的变狼进行赋值。
在初始化阶段会执行类构造器<cinit>,类构造器并不是程序员书写的,而是javac收集类变狼赋值
语句和static代码块而生成的。而且静态代码块只能访问到定义在静态代码块之前的变量。
<cinit>对于类和接口不是必须的,对于类来说,如果没有静态代码块和类变量赋值语句,就不会
有类构造器;对于接口来说,因为不支持static代码块,所以如果没有变量赋值,也不会有
tip: 因为在子类初始化的时候,如果父类未初始化,那么先初始化父类。所以第一个执行的类构造器
一定是java.lang.Object的;但是接口不同,接口只有父接口中定义的变量被使用的的时候,
才会被初始化。
线程安全
java保证,<cinit>的线程安全,多个线程初始化一个类,则cinit只会被执行一次。所以我们说
在多线程环境下,static变量的赋值和static代码块中的语句都是线程安全的。
类的卸载
类的卸载需要满足以下条件
-
该类的所有实例化对象都已经被GC
-
该类的类加载器语已经被GC
-
该类没有在其他任何地方存在引用
需要注意的时候,即使满足了类的卸载条件,类也不一定会被卸载。由java自带的类加载器加载
的类是不会被卸载的。