Java虚拟机类加载机制
定义
虚拟机把描述类的数据从Class文件加载到内存,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载的过程
类的加载过程分为5个步骤:加载、验证、准备、解析、初始化
其中的验证、准备、解析阶段又统称为连接,如下图所示。
在这5个阶段中,加载、验证、准备、初始化这4个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定,为了支持java语言的运行时绑定,它在某些情况下可以在初始化阶段之后再开始。
这里之所以说按部就班地开始,而不是按部就班地“进行”或“完成”,是因为这些阶段通常可以交叉混合进行,如在加载阶段执行过程中,也会同时执行验证阶段。
类加载的时机
什么时候要开始一个类的类加载?
什么情况下需要开始类加载过程的第一步“加载”?Java虚拟机并没有进行强制约束,这点可以由虚拟机自行实现。
但是对于初始化阶段,虚拟机规范则进行了严格规定,有且只有在以下7种情况下,必须立即对类进行初始化(而加载、验证、准备自然需要在此之前开始):
- 使用new关键字实例化对象的时候
- 设置或读取一个类的静态字段(被final修饰,已在编译器把结果放入常量池的静态字段除外)的时候
- 调用一个类的静态方法的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 初始化一个类的子类(会首先初始化父类)
- 当虚拟机启动的时候,初始化包含main方法的主类
- 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
类加载过程详解
加载
在加载阶段,虚拟机需要完成以下3件事情:
- 通过一个类的全限定名来获取此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口(虽然是对象,但并没有明确规定要存在java堆中,HotSpot的实现是存在了方法区)
验证
验证阶段大致分4个阶段:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
此阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面3个阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。(从这里也可以看出,验证阶段是和加载阶段一起进行的,只有当验证阶段完成后,加载阶段的第二个步骤将字节流转储为方法区的运行时数据结构才能完成)
准备
准备阶段是正式为类变量分配内存并设置类变量初始值。
需要注意的是,类似于:
“public static int value=123” 这种定义,在准备阶段过后的初始值是0而不是123,把value赋值为123需要等到初始化阶段再执行。
但是如果是:
“public static final int value=123” 这种定义,编译时javac会给value生成ConstantValue属性,这种情况下在准备阶段虚拟机就会根据ConstantValue将value赋值为123。
解析
解析阶段是虚拟机将常量池内的符号引用转化为直接引用的过程。
主要有4种:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化
初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法是由编译器自动收集类中的静态变量赋值动作和静态语句块(static{})中的语句合并生成的,编译器的收集顺序由语句在源文件中出现的顺序决定。
需要注意的有两点:
- 在执行类的<clinit>()方法之前,如果类有父类,则先执行父类的<clinit>()方法
- 与类不同,在执行接口的<clinit>()方法之前,如果接口有父接口,不需要先执行父类的<clinit>()方法。
类加载器
在类加载的第一个阶段--“加载”阶段,第一个动作是:“通过一个类的全限定名来获取此类的二进制字节流”,我们把实现这个动作的代码模块称为“类加载器”。
类加载器最初是为了满足Java Applet的需求而开发的,虽然目前Java Applet基本已经“死掉”,但是类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为Java体系中的一块重要基石。
必须了解的一个概念是:
对于一个类,这个类本身和加载它的类加载器一同确立其在Java虚拟机的唯一性。
也就是说,不同类加载器加载同一个Class文件所产生的两个类是不相等的,体现在Class对象的equals()方法、instanceof关键字的判定等。
类加载器分类
- 启动类加载器(Bootstrap ClassLoader):负责将JAVA_HOME/lib 目录下的,或者被-Xbootclasspath参数所指定的路径中的,被虚拟机识别的类库(仅按文件名识别,如rt.jar,名字不符合的类库不会加载)加载到虚拟机内存中。启动类加载器无法被Java程序直接引用。
- 扩展类加载器(Extension ClassLoader):加载JAVA_HOME/lib/ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):也成为系统加载器。负责加载用户类路径(classpath)下所指定的类库,开发者可以直接使用这个类加载器。如果应用程序中没有自定义自己的类加载器,一般这个就是程序中默认的类加载器。
双亲委派模型
如图:
图中的类加载器之间的层次关系,称为类加载器的双亲委派模型。
双亲委派模型要求除了顶层的启动类加载器之外,其他的加载器都要有自己的父加载器。这里类加载器之间的父子关系不以继承而是以组合方式来实现。
双亲委派模型的工作过程是:每一个类加载器收到类加载请求,都会首先将请求委派到其父加载器去完成,只有当父加载器无法完成加载,子加载器才会尝试自己去加载。
双亲委派模型的好处是Java类随着它的加载器一起具备了优先级的层级关系。如java.lang.Object,它存在于rt.jar,无论哪个类加载器要加载这个类,最终都要委派给顶层的启动加载器,因此Object在程序的各类加载器环境中都是同一个类。即使用户自己定义一个java.lang.Object类,也无法被加载。