虚拟机类加载机制
2020-02-16 本文已影响0人
官子寒
1. 概述
虚拟机把描述类的数据从
Class
文件加载到内存,并对数据进行校验,转换和解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是类加载机制
2. 类加载的时机
类加载的时机初始化的四种情况:
- 遇到
new
、getstatic
、putstatic
或invokestatic
这4条字节码指令时,如果类没有进行初始化,则需要先触发其初始化 - 使用
java.lang.reflect
包的方法对类进行反射调用的时候 - 当初始化一个类的时候,如果发现其父类还没有初始化,需要先对父类进行初始化
- 当虚拟机启动时,用户需要指定一个主类,虚拟机会先初始化主类
- 当使用JDK1.7的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且这个方法句柄所对应的类未进行初始化,需先触发其初始化。
注意
- 对于静态字段,只有直接定义这个字段的类才会被初始化
- 通过数组类定义引用类,不会导致引用类的初始化
- 常量直接进入常量池,因此本质上不会引用类,不会导致初始化
3. 类加载的过程
3.1 加载
分为3个步骤
- 通过一个类的全限定名来获得定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在Java堆中生成一个代表这个类的
java.lang.Class
对象,作为方法区这些数据的访问入口
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区的数据存储格式由虚拟机自己定义,虚拟机规范未规定此区域的具体数据结构。然后在Java堆中实例化一个java.lang.Class
的对象,这个对象将作为程序访问方法区中的哲学类型数据的外部接口
3.2 验证
验证是连接的第一个阶段,目的是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
包括4个阶段:
- 文件格式验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。经过这一阶段后,字节流才会进入内存的方法区进行储存
- 元数据验证:对字节码描述进行语义分析,以保证其描述的信息符合Java语言规范的要求
- 字节码验证:主要工作是进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为
- 符号引用验证:目的是确保解析动作能进行,符号引用可以安全转化为直接引用
3.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都主要在方法区进行
赋零值
public static int value = 123;
赋值123,因为value直接进入方法区的常量池,在其中二进制流中有存储ConstantValue属性,所以在会在准备阶段就被初始化为该值
public static final int value = 123;
3.4 解析
解析阶段就是将符号引用转化为直接引用的过程
- 符号引用(Symbolic References):以一组符号来描述所引用的目标。
- 直接引用(Direct References)
3.5 初始化
类加载过程的最后一步,会开始真正执行类中定义的Java字节码。而之前的类加载过程中,除了在『加载』阶段用户应用程序可通过自定义类加载器参与之外,其余阶段均由虚拟机主导和控制。
<clinit>()
方法执行的细节:
- 由编译器自动收集类中所有的类变量的赋值动作和静态语句块中的语句合并而成,编译器搜集顺序一定是先赋值,再初始化块
- 与类的构造函数
<init>()
不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()
执行之前,父类的<clinit>()
方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()
方法肯定是Object - 父类的静态初始化块先于子类执行
- 接口也会有
<clinit>()
方法,但与类不同的是,接口的<clinit>()
不需要先执行父接口的<clinit>()
,只有当父接口的变量被使用时才会 - 虚拟机会保证一个类的
<clinit>()
在多线程环境中被正确地加锁和同步
4. 类加载器
只用于实现类的加载动作,但在程序中的作用远不止于类加载阶段
判定类:类Class文件是否相同 + 类加载器是否相同
4.1 类加载器分类
类加载器:
- 启动类加载器
- 由C++语言实现,是虚拟机自身的一部分。
- 负责加载存放在<JAVA_HOME>\lib目录中、或被-Xbootclasspath参数所指定路径中的、且可被虚拟机识别的类库。
- 无法被Java程序直接引用,如果自定义类加载器想要把加载请求委派给引导类加载器的话,可直接用null代替。
- 扩展类加载器
- 由sun.misc.Launcher$ExtClassLoader实现。
- 负责加载<JAVA_HOME>\lib\ext目录中的、或者被java.ext.dirs系统变量所指定的路径中的所有类库。
- 应用程序类加载器
- 是默认的类加载器,是ClassLoader中的getSystemClassLoader()的返回值,故又称为系统类加载器。
- 由sun.misc.Launcher$App-ClassLoader实现。
- 负责加载用户类路径上所指定的类库。
4.2 双亲委派模型
- 表示类加载器之间的层次关系。
- 前提:除了顶层启动类加载器外,其余类加载器都应当有自己的父类加载器,且它们之间关系一般不会以继承(Inheritance)关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
- 工作过程:若一个类加载器收到了类加载的请求,它先会把这个请求委派给父类加载器,并向上传递,最终请求都传送到顶层的启动类加载器中。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
注意:不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。 - 优点:类会随着它的类加载器一起具备带有优先级的层次关系,可保证Java程序的稳定运作;实现简单,所有实现代码都集中在
java.lang.ClassLoader
的loadClass()
中。