虚拟机类加载机制学习笔记
虚拟机加载机制是虚拟机把描述类从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
类加载的过程可分为五个阶段:加载、验证、准备、解析和初始化。
这五个阶段的顺序是确定的,并不是说只有上一个阶段执行完才会开始执行下一个阶段,这些阶段是交叉进行的,在一个阶段进行的过程中可能交叉进行着另一个阶段过程,也就是说只是它们的开始顺序确定,但是进行和结束的顺序并不是确定的。下面将按照类加载的五个阶段对其进行介绍。
加载
加载阶段虚拟机主要3件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个二进制字节流所代表的静态存储结构转换为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象(虽然是对象,但是存放在方法区中),作为程序访问方法区各种类型数据的外部接口。
值得注意的是非数组类和数组类在加载阶段情况有所不同:非数组类的加载阶段(准确的说是加载阶段中获取类的二进制流的动作)可控性比较强,可以使用系统提供的引导类加载器来完成,也可以自己定义;而数组类本身不通过加载器创建,它是由虚拟机直接创建,但是数组类的元素类型仍然需要加载器去创建。数组类创建规则如下:
1) 如果数组的组件类型(数组中元素的类型)是引用型,则递归采用加载过程去加载这个组件类型,此数组将在加载组件类的加载器的类名称空间上被标识。
2)如果数组的组件类型不是引用类型,数组标记为与引导类加载器关联。
3) 数组类的可见性和它的组件类型的可见性一致,如果组件类型为引用类型,则数组的可见性默认为public。
验证
验证是连接的第一步,这一步的目的是为了保证Class文件的字节流中包含的信息符合虚拟机的要求,并不会危害到虚拟机的安全。(Class文件可以不由Java源码编译而来,可以由任何途径产生,所已验证这一阶段就变得尤为重要)
验证可以分为四个阶段:
- 文件格式验证:
这一阶段的目的是保证输入的字节流能正确解析并存储到方法区之中,格式上符合一个Java类型信息的要求;只有经过这个阶段,字节码才能进入方法区中并进行存储(存储为方法区的存储结构,后续三个验证阶段都是根据这一存储结构进行的)。 - 元数据验证:
这一阶段是对字节码描述的信息进行语义校验,以确保其描述的信息符合java语言规范。 - 字节码验证:
目的通过数据流和控制流的分析,确定程序语义是否合法和符合语义。(实际上就是去校验数据流所描述程序的逻辑是否正确) - 符号引用验证:
此阶段发生在虚拟机将符号引用转化为直接引用的时候。可以看作对类自身以外的信息进行匹配性验证。
验证阶段并不是必要的,如果运行的全部代码都已进行过反复使用或验证,则可以设置参数-Xverify:none来关闭大部分验证,来缩短类加载的时间。
准备
准备阶段是正式为类变量(被static修饰的属性)分配内存和设置初始值的阶段,这些变量使用的内存将在方法区中进行分配。
初始值的设置分为两种情况:
- 通常情况下初始值设置为零值。例如:public static int a=1;对a设置的初始值为0而不能为1,将a赋值为1的操作实在初始化阶段进行的。
- 当类中的属性存在ConstantValue类型时(即用final修饰的属性)设置的初值为属性的右值。例如:public static final int b=1;编译时将会为b生成ConstantValue属性,虚拟机会根据ConstantValue属性将b设置成1。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用与直接引用的关联:
- 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能准确的定位到目标就行。符号引用与虚拟机无关,引用的目标甚至可能未加载到虚拟机。
- 直接引用:可以是直接指向目标的指针、相对偏移量或间接定位到目标的句柄。直接引用的目标必然已经存在与虚拟机之中。
同一个符号引用进行多次解析请求的问题:
除invokedynamic指令以外,虚拟机可以实现对第一次解析的结果进行缓存(在运行池常量中记录直接引用,并把常量标识标记为以解析)从而避免重复解析。
而invokedynamic指令触发过解析的符号引用时,不能使用上一种情况。由于是动态的进行解析,解析的结果可能一直在改变,必须运行到这条指令时解析动作才会进行。
解析动作针对的符号引用:类或接口、字段、类方法、接口方法、方法类型、方法句柄和动态调用点限定符。具体内容见《深入理解java虚拟机第二版》p221页。
初始化
初始化阶段是执行类构造器()方法的过程。()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的(static{}块)中的语句合并而成。编译器收集的顺序是由语句在源程序中出现的顺序决定的。在静态语句块中只能访问到它之前定义的静态语句;对于它之后出现的语句只能进行赋值操作,而不能进行访问。有以下几点细节需要注意:
- 在虚拟机在执行()方法之前,如果这个类有父类,则先执行父类的()方法,因此虚拟机第一个被执行()的类一定是java.lang.Object。
- 如果这个类中不包含类变量或没有赋值操作,则编译器可以不为这个类产生()方法。
- 接口中不能使用静态语句块,但是可以使用静态变量。
- ()方法是线程同步的。
虚拟机规范了有且只有5中情况必须立即进行初始化
- 遇到new、getstatic、putstatic或invokestatic这4条指令的时候,如果类没进行初始化,则需要先出发初始化。
- 使用java.lang.reflect包的方法进行反射调用的时候,需要先进行初始化
- 初始化一个类时,如果其父类没有进行初始化,则先要对其父类进行初始化。
- 虚拟机启动时,要执行的主类(包含main()方法的那个类),须先进行初始化。
- 使用JDK1.7的动态语言时,如果一个java.lang.invoke.MethodHandle实例后的结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄时,须先进行初始化。
到这里类加载的五个阶段已经全部介绍完毕,后面是对类加载机制的一些补充。
类与类加载器
一个类需要由类加载器和它本身共同来确定其在虚拟机中的唯一性,也就是说即使是同一个类,如果加载它们的加载器不同,这两个类也不能说是相等的。
双亲委派模型
类加载器分为3类:
- 启动类加载器:这个加载器主要负责将存放在<JAVA_HOME>/lib目录中的,或者-Xbootclasspath参数指定的路径中的类库加载到虚拟机内存中。
- 扩展类加载器:加载<JAVA_HOME>/lib/ext目录,或者java.ext.dirs系统变量所指定的路径中的类库。
- 应用程序加载器:负责加载用户类路径(ClassPath)上所指定的类库。
双亲委派模型如上图所示,除了最顶层的启动类加载器外,其他加载器都有自己的父类,并且这种父子关系不是通过继承来实现的,而是通过组合来复用父类加载器。
双亲委派模型的工作过程:当一个加载器收到加载请求的时候,并不会自己去尝试加载这个类,而是将加载请求委派给它的父类加载器去完成,每一层加载器都是如此,最终加载请求都会传送到顶层的启动类加载器,只有当父类加载器反馈自己无法完成这个加载的时候,子类加载器才会尝试加载这个类。