jvm加载类的加载过程详解

类的加载过程详解

加载

  1. 加载时类加载的一个阶段,不是类加载。
  2. 首先是通过全限定类名获取定义这个类的二进制字节流。
  3. 将这个字节流中所代表的静态存储结构转换为方法区的运行时数据结构,也就是将静态资源放置在方法的运行时常量池中。
  4. 再内存中生成一个这个类的class对象,作为方法区中这个类的各种数据访问入口。
    //数组和非数组对象的加载略有不同,稍后补充

验证

  1. 首先要进行的是文件格式验证,验证是否能够被虚拟机处理解析:
    (1) 是否是以魔数0xCAFEBABE开头。(魔数:每个class文件的头四个字母,作用就是确定这个文件是不是能被jvm解析的class文件。)
    (2) 主、次版本号是否在当前虚拟机的处理范围之内。
    (3) 常量池中是否存在不能被解析的常量类型(检查常量tag标志)。
    (4) 指向常量的各种索引值是否有指向不存在或者不符合类型的常量。
    (5) CONSTANT_Utf8_info型的常量中是否有不符合utf-8的数据。
    (6) class文件的各个部分或者本身文件是否存在被删除或者附加的其他信息。
  2. 元数据验证:对字节码描述的信息进行语义分析,保证描述信息符合java语言规范
    (1) 这个类是否有父类,除了Object外,其他类都应该有父类。
    (2) 这个类的父类是否继承了不可被继承的类(final修饰的类)。
    (3) 如果这个类不是抽象类,是否实现了其父类或者接口中所有要求实现的方法。
    (4) 类中的方法或者字段是否与父类发生矛盾(例如覆盖了父类中final修饰的字段,或者出现了不合规则的重载:例如参数一致但是返回值不同)
  3. 字节码验证
    字节码验证是验证过程中最复杂的一个,主要是通过数据流和控制流进行分析,确保程序的语义是合法的、符合逻辑的。在元数据验证对数据类型的验证之后,这个验证对类的方法体进行校验分析,确保被校验类方法在运行时不会对虚拟机进行危害事件。例如:保证任何时刻操作数栈的数据类型都能与指令代码序列配合工作,不会出现类似于在操作数栈放置了一个int类型的数据,使用时却按照long类型来加载进本地变量表中。
    保证跳转指令不会跳转到方法体以外的字节码指令上。
    保证方法体中的类型转换是有效的,例如:把一个子类数据类型赋值给父类数据类型,这是安全的,到那时把父类数据类型赋值给子类数据类型,甚至于赋值给一个毫无继承关系,甚至完全不相干的数据类型时,这种做法是危险的。
    如果一个类的方法体中的字节码没有通过字节码验证,那肯定是有问题的;即便是一个方法体中的字节码通过字节码验证,也不能代表是一定安全的。即使字节码验证中进行了大量的验证,也不能保证这一点。通过程序去校验程序逻辑无法做到绝对准确的,不能通过程序准确的计算程序能否在有限的时间之内运行结束。
    由于数据流验证的复杂性,虚拟机设计团队为了避免过多的时间消耗在字节码验证阶段,在jdk1.6之后的Javac编译器和Java虚拟机中进行了一次优化,给方法体中的code属性的属性表中增加了一项名为“StackMapTable”的的属性这项属性描述了方法体中所有的基本块开始时本地变量表和操作数栈中应有的状态,在字节码验证期间,就不需要程序推导来验证这些状态的合法性,只需要检查stackMapTable属性的记录是否合法,将字节码验证的类型推导转为类型检查节约时间。
    在jdk1.6之后提供了**-XX:-UseSplitVerifiter**选项来关闭这项优化,或者使用-XX:+FailOverToOldVerifiter要求在校验失败后退回到旧的类型推导方式进行校验。在jdk1.7之后,对主版本号大于50的class文件。使用类型检查是唯一的选择,不允许退回到类型推导的校验方式。
  4. 符号引用验证
    发生在将符号引用转换为直接引用的时候,符号验证可以看作对类自身以外的信息(常量池中的各种符号引用)进行匹配校验。
    (1) 符号引用通过字符串描述的全限定名能否找到对应的类。
    (2) 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
    (3) 符号引用中的类、字段、方法的访问性(pubilc、protected、defalut、private)是否可被当前类访问。
    符号引用验证的目的是确定解析动作能正常执行,如果无法通过符号验证,则会抛出java.lang.IncompatibleClassChangeError异常的子类:如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。
    对于类加载机制来说,符号引用验证是一个非常重要但是不是非常必要的阶段,因为对程序运行期没有影响。如果所运行的全部代码(包括自己编写的及第三方的代码)都已经被反复使用和验证过,也可以在实施阶段就考虑使用-Xverifiter:none参数进行关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

这个是正式为类变量分配内存并设置类变量初始值的阶段,这个变量所使用的内存都是方法区中进行分配。首先,这个时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量,实例变量是类加载的时候加载进堆里的。其次,这个时候的变量初始值基本都是零值,比如在定义一个变量的时候赋值了,那也是在程序编译之后,存放在类构造器的方法之中,所以要在初始化之后才会等于所赋的值,在准备阶段依然是0。

public static int value = 100;

这个时候依然是0。
也有特殊情况,在类字段的字段属性表上存在ConstantValue属性值,那么在准备阶段变量就会初始化为ConstantValue所指定的值。

public static final int value = 100;

这个阶段就会将value赋值100。

解析

解析阶段是虚拟机将常量池中的符号引用转换为直接饮用的过程。
符号引用:符号应用是以一种符号来描述所引用的目标,符号可以是任何形式的字面量,只要在使用时能够无歧义的定位到目标即可。符号应用与虚拟机实现的内存布局无关,引用的目标不一定非要加载进内存中。各种虚拟机所实现的内存布局各不相同,但是他们能接受的符号引用必须一致,因为符号引用的字面量形式明确定义在Java虚拟机规范的class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量、或者一个能间接定位到目标的句柄。直接引用是和虚拟机所实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那么引用的目标必定已经在内存中存在。
解析规则:对一个符号引用多次解析很常见,除了invokedynamic指令,虚拟机可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并标记为以解析的状态)从而避免解析动作的重复请求。无论是否执行的多次的解析请求,虚拟机需要保证的是在同一个实体中,如果一个符号引用已经被成功解析了,那么后续的引用解析也应该同样成功;同样,如果一个符号引用在第一次解析的时候失败了,那么在后续解析中也应该收到相同的异常。
invokedynamic指令:对于invokedynamic指令,上面的规则并不成立,当碰到某个被invokedynamic指令触发解析的符号引用时,并不意味这个解析结果对其他的invokedynamic指令也同样使用,因为invokedynamic指令的目的就是为了动态语言支持(只使用Java语言不会触发这条字节码指令),他所对应的引用称为动态调用点限定符,这里动态的含义代表必须是程序实际运行到这条指令的时候,解析动作才能进行;相反,其余可以触发解析的指令都是静态的,可以在刚刚完成加载阶段,还没有开始执行代码时进行解析。
虚拟机并未规定解析发生时间,只要求在使用操作符号指令之前完成解析即可。
解析动作主要是针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类符号引用进行。(分别对应于常量池的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT7中常量类型)

  1. 解析类或者接口:假设当前代码为D,要将一个未曾解析过的符号N解析为一个类或者接口C:
    (1) 首先如果C不是一个数组类型,那么虚拟机会将N的全限定类名传递给D的类加载器去加载C,在加载过程中,由于元数据验证或者字节码验证可能引起的其他验证,例如加载这个父类或者实现的接口。一旦加载过程中出现异常,解析过程就宣告失败。
    (2) 如果C是一个数组,并且数组的类型是一个对象,那么N的描述符会是Ljava.lang,Integer形式,那么会按照(1)去加载,如果元素类型是java.lang.integer的形式,那么虚拟机会创建一个代表此数组维度和元素的数组对象。
    (3) 如果上面步骤未出现问题,C在虚拟机中已经成为一个类或者接口,那么将调用符号引用验证,验证D是否对C具备访问权限,如果不具备,会爆出java.lang.IllAccessError异常。
  2. 字段解析:要解析一个未曾被解析的字段,首先将会对字段表中class_index项中索引的CONATANT_Class_info符号引用进行解析,也就是先将字段所属的类或者接口进行解析,只有解析类或者接口的时候成功完成,那么这个字段所属的类或接口为C表示。
    (1) 如果C中包含了简单名称和字段描述符与目标字段相匹配的字段,则返回这个字段的直接引用。
    (2) 如果C继承了一个接口,将会按照继承关系从下往上递归搜索各个接口中有没有包含简单名称和字段描述符相匹配的字段,如果有就返回。
    (3) 如果C不是java.lang.object,那么将会按照继承关系从下往上递归搜索其父类,如果在父类中搜索到了简单名称和字段描述符相匹配的字段,则返回。
    (4) 如果本类或者接口中以及父接口和父类中都不存在,则报java.lang.NosuchFieldError。
    成功返回之后将会进行权限验证,确定具备字段访问权限。
  3. 类方法解析:第一步和字段解析一样,都是需要对class_index项中索引中方法所属的类或者接口进行解析。
    (1) 类方法和接口方法符号引用的常量类型是分开的,不是同一个类型,所以如果发现所在类是一个接口,直接返回java.lang.IncompatibleClassChangeError异常。
    (2) 如果本类中含有简单名称和描述符都相匹配的方法,那么就直接返回。
    (3) 在父类中通过递归以下往上层次查询,如果有简单名称和描述符都相匹配的方法,则返回。
    (4) 如果在类所实现的接口以及其父接口中递归查找到了简单名称和描述符都相匹配的方法,说明这个类是一个抽象类,返回java.lang.AbstractMethodError。
    (5) 如果都没查到,就宣布查找失败,返回java.lang.NoSuchMethodError异常。
    查找到了也要在进行权限验证。
  4. 接口方法解析:跟字段解析和类方法解析一致,都许哟啊通过class_index中的索引属性对方法所属的接口或者类进行解析。
    (1)类方法和接口方法常量类型不同,如果发现接口方法所处的接口是一个类,报java.lang.IncompatibleClassError异常。
    (2) 在接口中查找有没有简单名称和描述符相匹配的方法,有则返回。
    (3) 在父接口中以递归的形式查找,直到查找完object类,判断是否存在简单名称和描述符都相匹配的方法,有则返回。
    (4) 如果都没有,报java.lang.NoSuchMethodError异常。
    接口方法默认都是public,所以不存在权限问题。

初始化

初始化是加载的最后一个阶段,前面除了加载的时候用户可以自定义类加载器,其他都是由虚拟机主导,到这初始化才开始正式执行Java代码(转换后的字节码)。在准备阶段,对变量已经赋值过一次初始值了,初始化就是通过程序来制定的计划主观的去初始化变量。初始化阶段是类构造器方法的过程。
初始化方法是由编译器自动收集类中所有类变量的复制动作和静态语句块合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块智能访问定义在它之前的变量,不能访问定义在它之后的,按顺序加载,不然会报非法向前引用
类构造器方法和类中的构造函数不同,类构造器方法并不需要显示的调用,虚拟机会在加载类构造器方法之前,加载其父类的构造器方法,所以java.lang.Object是最先加载的。由于父类的构造器方法先执行,也就代表父类的静态代码块要优于子类的赋值变量操作。如果一个类里没有静态代码块,也没有变量赋值操作,编译器会将生成类构造器方法这一步省掉。
接口中没有静态代码块,只有变量赋值操作,所以都会生成构造器方法,但是不同的是接口不需要先加载其父接口的构造器方法,它的实现类在加载的时候也不会记载接口的构造器方法,只有当它定义的变量使用的时候才会加载。
在类或接口的构造器方法中,会被虚拟机在多线程的环境下添加锁,同步,保证只有一个线程在加载,其他线程属于堵塞状态。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值