加载
加载阶段是类加载的过程的一个阶段,在加载阶段虚拟机需要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转化为方法区的运行时存储结构
- java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口
加载完成后外部的二进制字节流就按照虚拟机所需的格式存储在方法区,方法区中的数据存储格式由虚拟机自行定义。
验证
虚拟机如果不检查输入的字节流,对其完全信任很有可能因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
具体检查哪些方面,何时检查虚拟机规范没有要求或者明确说明,所以不同虚拟机对类验证实现可能有所不同,大致可以分为四个过程:文件格式验证,元数据验证,字节码验证和符号引用验证
文件格式验证
这一阶段要验证字节流是否符合Class文件规范,并且能被当前版本虚拟机处理,所以这一阶段主要包含以下验证点:
- 是否以魔数0XCAFFBABY开头
- 主次版本号是否在虚拟机处理范围内
- 常量池中的常量是否有不支持的类型(检查TAG)
- 直线常量的各种索引值中是否有指向不存在的常量或者不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合utf8编码规范的数据
- Class文件各个部分以及文件本身是否有本删除的或附加其他信息
- 。。。。。。
经历了这个阶段的验证后,字节流才会流进内存的方法区进行存储,后续三个阶段是基于方法区的存储结构进行的。
元数据验证
第二阶段是对字节码描述信息进行语义分析,以保证其描述的信息符合java语言规范,可能包括的验证点如下:
- 这个类是否有父类(除了java.lang.Object,所有的类都应该有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口的中要求实现的方法
- 类中字段,方法与父类产生了矛盾(例如覆盖了父类中final字段,或者出现不符合规范的方法重载,例如方法参数一致,返回类型不同)
- 。。。。。
第二阶段主要目的是对类的元数据信息进行语义校验,保证不存在不符合java语言规范的元数据信息
字节码验证
第三阶段最为复杂,主要是对数据流和控制流进行分析,主要有以下内容:
- 保证任意时刻操作栈的数据类型与指令代码序列都能配合工作,例如不会出现:在操作栈防止了一个int类型的数据,使用时却按long类型来载入到本地变量表中
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换都是有效的
如果一个方法体的字节码么有通过字节码验证,则肯定是有问题的;但如果一个方法通过了字节码验证,也不能说明其一定就是安全的。
符号引用验证
最后一个阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化将在链接的第三阶段-解析中发生。通常需要校验以下内容:
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 指定类中是否存在符合方法的字段描述及简单名称所描述方法和字段
- 符号引用中的类、字段是否可以被当前类访问
- ……
符号引用验证的目的是为了保证解析动作能够正常执行,如果无法通过符号验证,则会抛出IllegalAccessError,NoSuchMethodFieldError,NoSuchMethodError等。
验证阶段对于虚拟机的类加载机制来说,是非常重要的,但是不一定是必要的阶段。如果所运行的全部代码都已经是被反复使用和验证过,在实施阶段可以关闭大部分的类验证措施,以缩短虚拟机类加载时间。
准备
准备阶段是正式为类变量分配内存并正式设置类变量初始值的阶段,这些内存都将在方法区被分配。这里进行内存分配的仅仅包括类变量(被static修饰的变量),不包括实例变量,实例变量将在对象初始化随着对象一起被分配在堆中。
解析
解析阶段是虚拟机将内存池内的符号应用替换为直接引用的过程。
初始化阶段
初始化阶段是类加载过程中的最后一步,这一步才开始真正执行类中定义的Java程序代码。初始化阶段是执行《clinit》()方法的过程。《clinit》()方法细节和特点:
- 《clinit》()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static{}块)中的语句产生的,编译器收集的顺序是由语句顺讯在源文件中的顺序决定的,静态语句块只能访问到静态语句块之前的变量,定义在他之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
- 《clinit》()方法与类的构造函数不同,他不需要显示地调用父类构造器,虚拟机会保证父类的 《clinit》()在子类的 《clinit》()之前执行,因此中虚拟机第一个被执行 《clinit》()的是java.lang.Object
- 由于父类的 《clinit》()方法先执行,就意味着父类定义的静态语句快要优先于子类的变量赋值操作。
- 《clinit》()对于接口或者类不是必须的,如果一个类没有static快,则编译器可以不为这个类生成 《clinit》()方法
- 接口中不能有 static快,但仍然有变量初始化的复制操作,因此接口和类都会生成 ()方法。但是接口与类不同的是,执行接口的 《clinit》()方法,不需要先执行父接口的 《clinit》()。只有放父接口定义的变量被使用时候,父接口才会被初始化。另外,接口的实现类在初始化时候也一样不会执行接口的 《clinit》()方法
- 虚拟机会保证一个类的 《clinit》()方法在多线程环境被正确的加锁同步,如果多个线程同时初始化一个类,那么只有一个线程去执行这个类的 《clinit》()方法,其他都需等待,指导活动线程执行 《clinit》()方法完毕,如果一个类的 《clinit》()方法有很耗时的操作,那么就可能造成多个线程阻塞。