类的加载机制
我们知道,编译的过程是将java文件转换成class文件,也就是说我们在使用的时候,还是需要将class文件加载到我们的内存中我们才能使用的,这个过程就是类的加载机制,很多人会把这个知识点忽略掉,但是这个是非常重要的知识点,有助于我们去更深层次的学习java虚拟机的一些知识。今天我们就来学习一下类的加载机制。
类的加载机制的定义:
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是java虚拟机的加载机制。
类型的加载、连接和初始化过程都是在程序运行期间完成的。
类从被加载到虚拟机内存开始,到卸载内存为止,它的整一个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载。其中加载,验证,准备,初始化和卸载这5个阶段的顺序是确定的。
类的加载时机:
虚拟机并没有规定什么时候开始加载过程,但是对一下五种情况会立即进行初始化。
当然在初始化之前,一定执行了加载,验证,准备。
1.遇到new,getstatic,putstatic,invokestatic这4条字节码指令的时候,如果没有初始化,那么需要触发它们的初始化。
2.使用java.lang.reflect包的方法进行反射调用的时候,如果类没有初始化,那么需要初始化这个类。
3.初始化一个类的时候,如果这个类的父类没有初始化,需要对父类进行初始化。
4.虚拟机启动的时候,用户需要指定一个要执行的主类(含main()方法的类),虚拟机会先初始化这个类。
5.使用句柄的时候,这个类没有被初始化,先初始化这个句柄对应的类。
类的加载过程
1.加载:
定义:”加载“是”类加载“的其中一个过程,在加载的阶段虚拟机需要完成3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
备注:Class对象的实例表示正在运行的java应用程序中的类和接口。也就是说JVM中有N多个实例,每个类都有该Class对象,包括基本数据类型。
在第一点中获取字节流这一点要求并不是很具体。它没有指定从哪获取,也就是说不是一定从Class文件获取,可以从ZIP包获取,从网络获取,数据库中获取等等。相对其他过程来说,一个非数组类的加载阶段是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器类完成,也可以用用户自定义类的加载器去完成,开发人员可以定义自己的类加载器去控制字节流的获取方式(重写一个类加载器的loadClass()方法)。
加载阶段完成之后,虚拟机外部的二进制字节流就会按照虚拟机所需要的合适存储在方法区之中,方法区的数据存储格式由虚拟机自行定义,虚拟机规范没有规定这个区域的具体数据结构。在内存中生成一个代表这个类的java.lang.Class对象,**注意,这个对象是在方法区里面的,**作为方法区这个类的各种数据的访问入口。
2.验证:
定义:验证阶段是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
主要分为四个阶段:文件格式验证,元数据验证,字节码验证,符号引用验证。
文件格式的验证:
定义:这个阶段主要的目的是保证输入的字节流能正确的解析并存储于方法区之内。
- 是否以魔数0xCAFEBABF开头
- 主、次版本是否在当前虚拟机的处理范围
…
通过了这个阶段的验证之后,字节流就会存入方法区,后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
元数据验证:
对字节码描述的信息进行语义分析,以保证描述的信息符合java语言规范的要求。
- 这个类是否又父类
- 这个类的父类是否稽继承了不允许被继承的类
- 如果这个类不是抽象类,是不是实现了其父类或接口的所有方法
…
字节码验证:
定义:通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。对类的方法进行校验,保证被校验的类的方法在运行的时候不会做出危害虚拟机安全的事件。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中类型转换是有效的
…
如果一个类的方法体没有通过字节认证,那么他一定是有问题的,但是如果通过了字节码认证也不一定能证明他没有问题。
符号引用验证:
定义:这个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候。符号引用验证可以看作是对自身以外的信息进行匹配性校验。
- 符号引用中通过字符串描述的全限定名能否找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性是否可被当前类访问
3.准备:
定义:**准备阶段是正式为类变量(被stattic修饰的变量)分配内存并设置类变量初始值的阶段。**这里的初始值一般为数据类型的零值。例如
public static int value = 123
准备阶段之后的初始值是0而不是123,因为这个时候还没有开始执行任何的方法。将value赋值为123的是<clinit>()
方法
4.解析:
将常量池内的符号引用替换为直接引用的过程。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候能够没有歧义的定位到目标即可。引用的目标不一定已经加载到内存中。
直接引用:直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在了。
5.初始化:
初始化是加载过程中的最后一步,到了初始化阶段,才真正执行类中定义的Java程序代码。在准备阶段,类变量已经按系统的要求赋值过一次(零值),**而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。初始化阶段就是执行类构造器()方法的过程。
1.<clinit>()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态块中的语句合并生成的,编译器收集的顺序是按照在源文件中出现的顺序决定,静态语句块中只能访问定义在静态语句块前面的变量,定义在它之后的变量,在前面的静态语句块中可以赋值,但是不能访问。
2.<clinit>()
方法与类的构造函数(或者说实例构造函数<init>()
方法不同),它不需要显示的调用父类的构造器,虚拟机回保证父类在执行<clinit>()
方法之前,父类的<clinit>()
方法执行完毕。
3.由于父类的<clinit>()
方法先执行,也就意味着父类中定义的静态语句块要优于子类的静态语句块的赋值操作。
4.<clinit>()
方法对于类或者接口来说并不是必须的,如果没有静态语句块,也没有对变量赋值操作,那么编译器可以不生成<clinit>()
方法
5.虚拟机会保证一个类的<clinit>()
方法在多线程环境中被正确地加锁、同步,如果多线程同时去初始化一个类,那么只有一个线程去执行这个类的<clinit>()
方法。