Class文件加载过程

1.加载
  (1)通过类的全限定名来获取此类的二进制字节流
  (2)将此字节流代表的静态存储结构转化为方法区的运行时数据结构
  (3)在Java堆中生成一个代表这个类的Class对象,作为方法区这些数据的访问入口
  
  2.验证
  (1)文件格式验证 :验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理
   是否以魔数0xCAFEBABE开头
   主次版本号是否在当前处理机处理范围之内
   常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
   指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
   CONSTANT_Utf8_info类型的常量中是否有不符合UTF8编码的数据
   Class文件中各部分及文件本身是否有被删除或附加的其他信息
   ……
  2)元数据验证 :对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求
   这个类是否有父类(除了Object之外,所有类都应当有父类)
   这个类的父类是否继承了不允许被继承的类(被final修饰的类)
   如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
   类中的字段、方法是否与父类产生了矛盾(覆盖父类的final字段,不符合规则的方法重载)
   ……
  (3)字节码验证 :主要工作是进行数据流和控制流分析,保证被检验的类不会做出危害虚拟机的行为
   保证任意时刻操作数栈的数据类型与指令代码顺序都能配合工作
   保证跳转指令不会跳转到方法体之外的字节码指令上
   保证方法体的类型转换是有效的
   ……
  (4)符号引用验证 :在解析阶段中发生。
      符号引用验证可以看作是对类自身以外(常量池中各种符号引用)的信息进行匹配性的校验,通常需要校验以下内容:
   符号引用中通过字符串描述的全限定名是否能找到对应的类
   在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
   符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问
   ……
   可通过-Xverify:none来关闭大部分的类验证措施,以便缩短虚拟机类加载的时间
   
  3.准备
  准备阶段正式为类变量(static修饰的变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
  这里需要注意的是,不对实例变量分配内存,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。
  比如:public static int value = 123;
  这里所指的初始值是给value赋0(boolean是false,reference是null),而123是要在类初始化(clinit)中赋的。
  
 4.解析
  解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用以CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info等类型的常量出现。
  符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
   符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
  直接引用(Direct Reference):直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,
   同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
  解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池的CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info,CONSTANT_InterfaceMethodref_info
  四种常量类型。
  (1)类或接口的解析
   假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那虚拟机完成整个解析过程需要包括以下三个步骤:
   1)如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器其加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又将可能触发其他相关类的加载动作(父类或接口)。
   2)如果C是一个数组类型,并且数组的原属类型为对象,也就是N的描述符会是类型"[Ljava.lang.Integer"的形式,那将会按照第一点的规则加载数组元素类型。
    如果N的描述符如前面所假设的形式,需要加载的元素类型就是"java.lang.Integer",接着由虚拟机生成一个代表此数组维度和元素的数组对象。
   3)如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或者接口了,但在解析完成之前还要进行符号引用验证,确认C是否具备对D的访问权限。
  (2)字段解析
   要解析一个未被解析过的字段符号引用,首先将会对字段表内的class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或者接口的符号引用。
   如果解析完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤进行后续字段的搜索:
   1)如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
   2)否则,如果在C中实现了接口,将会按照继承关系从上到下递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
   3)否则,如果C不是Object的话,将会按照继承关系从上到下递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标匹配的字段,则返回此字段,查找结束。
   4)否则,查找失败,抛出NoSuchFieldError
   如果成功找到该字段,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出IllegalAccessError
  (3)类方法解析
   类方法解析的第一个步骤与字段解析一样,也要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索:
   1)类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引C是个接口,那么直接抛出IncompatibleClassChangeError
   2)如果通过了第1)步,在类C中查找是否有简单名和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
   3)否则,在类C的父类中递归查找是否有简单名和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
   4)否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明C是一个抽象类,这时候查找结束,抛出AbstractMethodError
   5)否则,宣告查找失败,抛出NoSuchMethodError
   如果成功找到了该方法,并返回直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出IllegalAccessError
  4)接口方法解析
   接口方法也是需要先解析出接口方发表的class_index项中索引的方法所属的类或者接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将按照如下的步骤进行后续方法搜索:
   1)与类方法相反,如果在接口方法中表中发现class_index中的索引C是一个类而不是接口,那就直接抛出IncompatibleClassChangeError
   2)否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
   3)否则,在接口C的父接口中递归查找,直到Object(查找范围会包括Object类)位置,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
   4)否则,宣告方法查找结束,抛出NoSuchMethodError
   由于接口中所有的方法都默认是public的,所以不存在访问权限问题
  5.初始化

    本过程在有任何调用本类的(参数或者方法)前提下才进行,如果仅仅是loadclass,不触发
  类初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java代码。
  在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者从另一个角度来说:初始化阶段是执行类构造器clinit方法的过程。
  1)clinit方法是由编译期自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译期收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在它之前的变量,
   定义在它之后的变量,在前面的静态语句块中能赋值,但不能访问。
  2)clinit方法与实例构造器init不同,它不需要显式的调用父类构造器,虚拟机会保证在子类的clinit执行之前,父类的clinit已经执行完毕。因此在虚拟机中第一个被执行的clinit方法的类肯定是Object
  3)由于父类的clinit方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
  4)clinit方法对于类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不生成clinit方法
  5)接口中不能使用静态语句块,但仍然有变量赋值操作,因此接口与类一样都会生成clinit方法。但是接口与类不一样,执行接口的clinit不需要先执行父接口的clinit。只有父接口的变量被使用时父接口才被初始化。
   另外,接口的实现类在初始化的时候也一样不会执行接口的clinit
  6)虚拟机会保证一个类的clinit在多线程环境中被正确的加锁和同步(执行一次)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值