JVM虚拟机夯实系列-虚拟机类加载的过程

类加载的过程

一、加载

加载阶段,虚拟机需要完成以下三件事:

通过一个类的全限定名获取定义此类的二进制字节流

将字节流的静态存储结构转化为方法区的运行时数据结构(静态存储结构——>运行时数据结构)。

内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的数据访问入口

非数组类的加载阶段

是开发人员可控性最强的阶段,这阶段可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。

数组类的加载阶段:

数组类本身不需要通过类加载器创建,由虚拟机直接创建,但数组类的元素类型(指数组去掉所有维度的类型需要加载器去创建。同时数组类的创建需要遵循以下的规则:

规则一:若数组类的组件类型(指数组去掉一个维度的类型,与元素类型不同)是引用类型,那就递归采用本节介绍的定义过程去加载组件类型,该数组会被标识在加载该组件类型的类加载器的类名称空间上。

规则二:若数组类的组件类不是引用类型,则虚拟机会把该数组标记为与引导类加载器关联,由引导类加载器处理。

数组类的可访问性其组件类型可访问性一致,若组件类型不是引用类型,它的数组类的可访问性将默认为public,可被所有的类和接口访问到。

    

    加载阶段完成后,加载进来的二进制字节流就按照虚拟机需要的字节存储方式存储在方法区,然后实例化一个java.lang.Class对象作为程序访问方法区中数据的外部接口。并且加载阶段连接阶段(一部分验证动作)是交叉进行的,但这两个阶段的开始时间严格进行的。

  • 验证

验证是连接阶段的第一步,虚拟机需要检查输入的字节流防止载入了有害的字节流使系统崩溃,所以验证这一步骤可以保证安全性,是虚拟机的一种自我保护的手段。那么验证主要分为4个阶段的动作:

①文件格式验证:这部分主要进行的是对文件格式验证,验证字节流是否符合Class文件结构的格式,能否被虚拟机读懂,从而保证字节流输入后能正确的被解析并且存储在方法区内。

这阶段进行验证的内容可能有以下几点:

·是否以魔数0xCAFEBABE“咖啡宝贝”开头。

·主、次版本号是否在当前Java虚拟机接受范围之内。

·常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。

·指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。

·CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据。

·Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。

·……等等等等

②元数据验证:这一部分的验证主要是及进行字节码描述的信息(比如数据类型)的语义分析保证其信息符合Java语言的规范要求

这阶段进行验证的内容可能包括:

·这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。

·这个类的父类是否继承了不允许被继承的类(被final修饰的类)。

·如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。

·类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。

·……等等等等

③字节码验证:该阶段是验证阶段中最复杂的一个阶段,主要是进行数据流和控制流分析来确定程序语义是合法的、符合逻辑的。第二阶段对元数据信息中的数据类型做完校验后,本阶段就会对类的方法体进行校验

这阶段进行验证的内容可能包括:

·保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作 栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况。

·保证任何跳转指令都不会跳转到方法体以外的字节码指令上。

·保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全 的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个 数据类型,则是危险和不合法的。

·……等等等等

虽说方法体通过字节码验证,但也不能说明其一定准确,因为“停机问题”——Halting Problem:通过程序去校验程序逻辑是无法做到绝对准确的。因为数据流验证的高复杂性,所以在JDK1.6后给方法体的Code属性增加了一项名为 “StackMapTable”的属性,这里记录了一些所有基本快的状态,在进行字节码验证时验证此部分是否合法即可,省略了大部分时间。

④符号引用验证:这个阶段是验证阶段的最后一个阶段,主要进行的工作是在虚拟机进行将符号引用转化为直接引用时(发生在解析阶段),对类自身以外的信息常量池中的各种符号引用)进行匹配性校验,也就是校验该类是否缺少或被禁止访问依赖的某些外部类、方法、字段等资源

这阶段进行验证的内容可能包括:

·符号引用中通过字符串描述的全限定名是否能找到对应的类。

·在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。

·符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问。

·……等等等等

  • 准备

准备阶段是为类变量被static修饰的变量分配内存并且对其设置初始值的阶段,这些变量使用的内存均在方法区进行分配,其中这些变量的初始值默认为0

【注】:

该阶段进行的是类变量的内存分配和赋值,实例化变量的内存分配Java堆中进行。

类变量(类的结构信息)——方法区

实例化变量(创建的对象和数组)——Java堆

以下为各个数据类型的默认零值:

  • 解析

解析阶段主要的工作内容是将常量池中的符号引用替换为直接引用

*符号引用:

符号引用是指以一组符号来描述所引用的目标,它可以是任何形式的字面量,只要能准确定位到所引用的目标即可。各个虚拟机的内存布局不同,但他们接受的符号引用必须一致,所以符号引用跟虚拟机的内存布局无关,同时其引用的目标不一定必须是已经加载到内存中的

*直接引用:

直接引用可以是直接指向目标的指针相对偏移量或是一个能间接定位到目标的句柄。与符号引用相反,直接引用与虚拟机的内存布局直接相关,所以同个符号引用在各个虚拟机翻译出来的直接引用一般不同,同时直接引用的目标必须已经存在在内存中。  

在解析工作进行时,虚拟机会判断在何时进行解析,有时会在类被加载器加载时对常量池中的符号引用进行解析,也有可能在一个符号引用被使用时对其进行解析。

同一个符号引用可能会被进行多次解析,这时会对第一次的解析结果进行缓存,将其得到的直接引用存储在常量池,并标记为已解析状态,这样就避免了重复解析的过程。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。  

  • 初始化

该阶段才真正执行类中定义的Java程序代码,前面阶段除了自定义类加载器外都是虚拟机主导进行。初始化阶段就是执行类构造器<clinit>()方法的过程,从而去初始化类变量和其他资源

以下是关于<clinit>()方法的介绍:

*<clinit>()方法是由编译器自动收集类中所有类变量的赋值静态语句块(static{}块)中的语句合并产生的。其中静态语句块只能访问到定义在静态语句块之前的变量,定义在其后的变量只能自静态语句块中赋值但不能访问

*<clinit>()方法不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行前父类的<clinit>()方法就已经执行完毕。在虚拟机中第一个执行<clinit>()方法的类肯定是java.lang.Object

*因为父类的<clinit>()方法先执行,所以父类中静态语句块要优先于子类变量赋值工作。

*<clinit>()方法对于类或接口来说不是必须的,若没有静态语句块也没有变量赋值操作,那么<clinit>()方法也可以不存在

*接口中没有静态语句块,但有变量的赋值操作。同时执行接口的<clinit>()方法时也不需要先执行父接口的<clinit>()方法,只有父接口中的变量被使用时,父接口才会初始化。同样的,实现类初始化时也不需要先执行接口中的<clinit>()方法。

*当多个线程同时去初始化一个类时,只有一个线程会执行<clinit>()方法。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值