JVM类加载机制

虚拟机把描述类的Class文件加载到内存中,并对数据进行校验、转换解析和初始化,最后形成能够被虚拟机直接使用的Java类型,叫做JVM的加载机制。

类的生命周期
  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化
  • 使用
  • 卸载

其中验证、准备和解析为连接过程;解析有可能在初始化之后进行,这是因为支持Java的运行时绑定。

必须进行初始化的五个场景

有且仅有以下五个场景,若类没有进行初始化,则必须立即初始化。

  1. 遇到newgetstaticputstaticinvokestatic四个指令时。这四条指令的场景一般为:使用new关键字创建对象时、读取或设置类的静态字段时(final修饰已在编译器放入常量池的字段除外)、调用类的静态方法时
  2. 对类进行反射调用时
  3. 初始化类时发现其父类还未初始化,先初始化父类。
  4. 优先初始化主类
  5. 当使用JDK1.7以后的动态语言支持时,若一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic方法的句柄

需要注意的是对于接口在初始化时并不一定要求其父接口也一定初始化,只有用到的时候才会进行初始化。
以上五种场景成为对类的主动引用,除此之外对类的引用不会引发类的初始化,成为被动引用。

三种被动引用

  1. 通过子类引用父类的静态字段,不会引起子类的初始化。只有定义被引用静态字段的类才会被初始化
  2. 创建类数组时不会引起类的初始化,如Test[] test = new Test[10];
  3. final static的常量被定义后会被存储到常量池,调用该常量时也不会初始化

类加载的过程

加载
  1. 通过类的全限定名获取到定义该类的二进制字节流
  2. 将字节流代表的静态存储结构转变为方法区的运行时数据结构
  3. 在内存中生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问接口

在非数组类的加载,读取二进制字节流可以使用开发人员自定义的类加载器进行
数组类本身不由加载器,由虚拟机动态创建,但数组类的元素类型(去掉所有维度)是由加载器创建的,数组类的创建遵循以下规则:

  • 若数组的组件类型(去掉一个维度)是引用类型,递归加载这个组件类型,数组C将在加载该组件的类加载器的类名称空间上被标识
  • 若数组的组件类型不是引用类型,JVM会将数组C标记为与引导类加载器关联
  • 数组类型与其组件类型可见性保持一致,若组件类型不是引用类型,则数组类可见性默认为public
验证

确保字节流信息符合虚拟机规范,不会危害虚拟机安全,非常重要,但非必要

  1. 文件格式验证:包括魔数、版本、类型符合等的验证,对字节流进行操作,保证字节流正确的解析并储存于方法区内
  2. 元数据验证:对字节码信息进行语义分析,保证符合Java语言规范。如是否有父类、是否继承了不允许继承的类等
  3. 字节码验证:最复杂的阶段,通过数据流和控制流,确定程序语义合法合逻辑。该阶段将对方法体进行分析,保证方法运行时不会危害JVM安全。如类型转换是否有效。JDK1.6改进后通过检查方法体Code属性表中的StackMapTable属性,变推到为检查,节省时间。
  4. 符号引用验证:对类自身以外的信息(常量池中的各种符号引用)进行匹配性校验。如符号引用中通过字符串描述的全限定名是否能找到对应的类。该阶段目的是保证解析动作能正常执行。
准备

正式为类变量分配内存并设置初始值(一般为零值,若存在ConstantValue属性,则初始化为ConstantValue指定的值),实例变量将在对象实例化时随对象一起在堆中分配。

解析

将符号引用替换为直接引用的过程

  • 符号引用:以一组符号来描述所引用目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标就可;与虚拟机内存布局无关,目标也不一定已经加载到内存中,符号引用的字面量形式明确定义在虚拟机规范Class文件格式中
  • 直接引用:能直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。与虚拟机内存分布有关,同一个符号引用在不同虚拟机实例中转换的直接引用一般不同,若已经存在直接引用,目标一定已经在内存中

解析阶段发生时间没有明确指定,只规定在16个操作符号引用的指令之前解析,所以可以选择在类加载时解析,也可以要使用符号引用前去解析。
对同一个符号引用多次解析的情况普遍,成功解析第一次后缓存到直接引用到常量池,要保证首次解析成功后续解析一直成功,反之亦然。(Invokedynamic指令例外)

解析动作主要包含以下几种:

  • 类或接口
  • 字段
  • 类方法
  • 接口方法
  • 方法类型
  • 方法句柄
  • 调用点限定符

类或接口
假设当前代码所处类为D,若要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,则解析需以下三个步骤:

  • 若果C不是一个数组类型,虚拟机会把代表N的全限定名传递给D的类加载器去加载C。加载过程中对元数据、字节码验证可能触发其他相关类加载动作,若出现任何异常,解析失败。
  • 若C不是一个数组类型且数组的元素类型为对象,会按照上一点的规则加载数组元素类型,接着虚拟机生成一个代表此数组维度和元素的数组对象
  • 若上面步骤无异常,C已经成为有效类或接口,解析前进行符号引用验证,确认D具备C的访问权限,不具备抛出java.lang.IllegalAccessError异常

字段解析
类或接口解析完成后,进行后续字段解析
返回条件:若接口或类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用

  • 若C本身就包含返回条件,查找结束
  • 否则,若C实现了接口,按照继承关系从下往上递归搜索接口和父接口,若包含返回条件,查找结束
  • 否则,若C不是Object,按照继承关系从下往上递归搜索其父类,若包含返回条件,查找结束
  • 否则,查找失败,抛出java.lang.NoSuchFieldError异常

若成功进行权限验证,若权限不足,抛出java.lang.IllegalAccessError异常。若一个字段同时出现在C的接口和父类中或在自己或父类的多个接口中出现(会造成冲突,除非自己进行覆盖),编译器会提示字段非法拒绝编译。

类方法解析
前提为类或接口解析成功,则进行以下几步进行后续的类方法解析

  • 若类方法表中发现C是一个接口,抛出异常
  • 若上步通过,C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  • 否则,在C的父类中递归查找是否有简单名称和描述符都与目标匹配的方法,有则返回该方法的直接引用,查找结束。
  • 否则,在C的接口列表及接口的父接口中递归查找是否有简单名称和描述符都与目标匹配的方法,若有,则C是一个抽象类,查找结束,抛出异常。
  • 否则查找失败抛异常。

接口方法解析
前提为类或接口解析成功,则进行以下几步进行后续的接口方法解析

  • 若接口方法表中发现C是个类不是接口,抛出异常。
  • 否则,在C中查找是否有简单名称和描述符都与目标匹配的方法,有则返回该方法的直接引用,查找结束。
  • 否则,在接口C的父接口中递归查找,知道Object类,是否有简单名称和描述符都与目标匹配的方法,有则返回该方法的直接引用,查找结束。
  • 否则,查找失败,抛出异常。
初始化

类加载最后一步,真正开始执行类定义的Java代码,该阶段是执行*类构造器<clinit>()*方法的过程。

  • <clinit>()方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块组合中的语句合并产生,顺序为语句顺序。静态语句块中只能访问到定义在静态语句块之前的变量,定义在其后的变量,只能赋值不能访问。
  • <clinit>() ≠ <init>(),后者是实例构造器。前者不需要显示调用父类构造器,虚拟机会保证子类的<clinit>()执行前父类的<clinit>()已经执行。因此父类中定义的静态语句块要优先于子类中的变量赋值操作。
  • <clinit>()对接口或类非必须,若没静态语句块或变量赋值操作,可以没有该方法。接口没有静态语句块但可能有初始化赋值操作,与类不同,接口在执行<clinit>()时可能只有在需要时才执行父接口的<clinit>()方法,接口实现类也可以在使用接口时才调用接口的<clinit>()方法。
  • <clinit>()方法线程安全,多个线程操作会产生阻塞。

类加载器

对于任意一个类,都需要加载它的类加载器和这个类本身一桶确立其再Java虚拟机的唯一性,每个类加载器都有一个独立的类名称空间,两个类是否相等的前提是两个类由同一个类加载器加载,否则必定不等。

双亲委派模型
  1. 启动类加载器(Bootstrap ClassLoader)
    使用C++实现,是虚拟机一部分。负责将存放在lib目录中的或者-Xbootclasspath参数指定的路径红的合法类库加载到JVM内存中。该加载器无法被Java程序直接引用。
  2. 扩展类加载器(Extension ClassLoader)
    负责加载lib\ext目录中的,或java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用该加载器。
  3. 应用程序类加载器(Application ClassLoader)
    ClassLoadergetSystemClassLoader返回值,又名系统类加载器,负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用,一般为程序默认加载器。

类加载器的父子关系一般不以继承实现,而是使用组合实现。
**双亲委派的过程为:**若一个加载器收到类加载请求,先将请求委派给父类加载器完成,若父类加载器无法完成,则再自己加载。
该过程对程序的稳定性和安全性很重要,防止与系统类重名从而出现多个不同的类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值