微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

jvm加载类的加载过程详解

@H_502_3@

类的加载过程详解

加载

  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、CONSTANT)

初始化

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐