Java虚拟机类加载机制

21 篇文章 0 订阅
5 篇文章 0 订阅

概述

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化、最终行程可以被虚拟机直接使用的类型,这个过程被称为虚拟机类加载机制。在Java语言里面,类的加载、连接和初始化过程实在程序运行期间完成的,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能的开销,但是却为java应用提供了极高的扩展和灵活性,Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。

类加载的时机

一个类从被加载到虚拟机内存中开始到卸载为止,它的整个声明周期将会经历加载(loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备、解析三个阶段部分统称为连接。加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这个顺序按部就班的进行。Java语言支持动态绑定,解析阶段可能再初始化阶段完成之后进行。
以下六中情况会立即对类进行初始化:

  • 遇到new、getstatic、putstatic、或invokestatic这四个指令的时候,如果类没有初始化则需要初始化。能够生成这四条指令的场景:使用new关键字实例化对象的时候;读取或者设置一个静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候;调用一个类的静态方法的时候。
  • 使用java.lang.reflect包的方法对其类型进行反射调用的时候,如果类型没有进行初始化,则需要先行初始化。
  • 当初始化类的时候、如果发现其父类还没有进行初始化,则需要先触发父类初始化。
  • 当虚拟机启动的时,用户需要指定一个执行主类,虚拟机会先初始化这个主类。
  • 当使用JDK 7新加入动态语言的支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析为REF_getStatic、REG_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应类没有进行初始化,则需要先触发其初始化。
  • 当一个接口定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载过程

1.加载

在加载阶段虚拟机主要完成一下三件事情:

  • 通过一个类的全限定名来获取定义类的二进制字节流。
  • 将这个字节流所代表的的静态储存结构转化为方法区的运行时数据结构。
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

相对于类加载过程的其他阶段,非数组类型的加载阶段(准确的说,是加载阶段获取类的二进制字节流的动作)是开发人员控制性最强的阶段。加载阶段可以使用Java虚拟机内置的引导类完成,也可以由用户自定义的类加载器完成。对于数组而言,数组本身不通过类加载器创建,它是由Java虚拟机直接在内存中动态构造出来。但是数组的元素类型最终还是要通过类加载器来完成加载。一个数组类(下面简称C)创建过程如下:

  • 如果数组的组件类型(Component Type,指的是数组去掉一个维度的类型)是引用类型,那就递归采用上述的定义的类加载过程去加载这个组件类型。数组将被标识在该组件类型的类加载器的类名称空间上。
  • 如果数组的组件类型不是引用类型,Java虚拟机将会把数组标记为与引导类加载器有关。
  • 数组类的可访问性与它的组件的可访问性是一致的,如果主键类型不是引用类型,它的数组的可访问性将默认为public,可被所有类和接口访问。

加载结束之后,Java虚拟机外部的二进制字节流将按照虚拟机设定的格式储存在方法区。

2. 验证

这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。验证阶段大致上会完成下面四个阶段的验证动作:

  • 文件格式验证,验证字节码是否符合Class文件格式规范,并且能被当前版本的虚拟机处理,例如是否以魔数0xCAFEBABE开头、主次版本号是否在当前Java虚拟机接收范围之内、常量池中是否有不被支持的常量类型(检查常量tag标志)、指向常量池的各种索引值中是否有指向不存在的常量或不符合类型的常量、CONSTANT_Utf8_info型的常量中是否有不符合UTF-8编码的数据、Class文件中各个部分及文件本身是否有被删除的或附加的其他信息等等。只有通过这个阶段的校验,这段字节码才被允许进入Java虚拟机内存方法区进行存储。
  • 元数据验证,对字节码描述信息进行语义分析,以保证其描述的信息符合《Java语言规范》的要求。例如:这个类是否有父类(除了java.lang.Object之外,所有类都应该有父类)、这个类的父类是否继承了不允许继承的类(final修饰的类)、如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法、类中的字段、方法是否与父类产生矛盾(例如覆盖父类的final字段,或者出现不符合规范的重载)等等。
  • 字节码验证,通过控制流和数据流分析,确定程序语义是合法、符合逻辑的,主要对类方法体(Class文件中的Code属性),保证被校验类的方法在运行时不会做出危害虚拟机的动作的行为,例如:保证任意时刻操作数栈的数据类型与指定代码序列都能配合工作、保证任何跳转指令都不会跳转到方法体以外的字节码指令上、保证方法体中的类型转换总是有效的等等。在JDK 6之后的javac编译器和java虚拟机里进行了一项联合优化,把尽可能多的校验挪到javac中进行,具体做法是给Code属性添加“StackMapTable”属性,java虚拟机只要校验StackMapTable的属性是否合法就可以了。
  • 符号引用验证、发生在虚拟机将符号引用转化为直接引用的时候,可以看做是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配校验。例如:符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定的类中是否符合方法字段描述符及简单名称所描述的方法和字段、符号引用中的类、字段、方法的可访问性是否可被当前类访问等等。

3. 准备

准备阶段是正式为类中定义的变量(即静态变量)分配内存并设置变量的初始值,不包含实例变量,实例变量将会在对象实例化的时候跟随对象一起分配在堆中。例如一个类变量定义为 public static int value = 123,其初始值是0而不是123,把value赋值为123的putstatic指令是程序编译之后,存放于类构造器()方法之中。小表是Java所有基本类型的零值:

数据类型零值
int0
long0
short(short)0
char‘\u0000’
byte(byte)0
booleanfalse
float0.0f
double0.0d
referencenull

通常情况下初始值为零,如果类字段的字段表属性中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue的值。

4. 解析

解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程,在Class文件中它以CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Methodref_info等类型常量出现。

  • 符号引用(Symbolic References):符号引用用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,应用的目标不一定是已经加载到虚拟机内存中的内容。
  • 直接引用(Direct References):直接引用指的是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用与虚拟机的内存实现有关。同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。

      Java虚拟机并没有规定解析阶段放生的具体时间,只要求执行anewarray、checkcast、getfield、gatstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、 invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符引用的字节码指令之前,先对他们所使用的符号引用进行解析。除了invokedynamic指令之外,虚拟机可以实现对第一次解析的结果进行缓存。
  Java虚拟机需要保证在同一个实体中,如果一个符号引用之前被解析成功,那么后续的引用解析请求就应当一直成功;同样的,如果一次解析失败,其它指令对这个符号的解析也应该收到一样的异常,哪怕这个请求的符号后来已成功加载进Java虚拟机内存中。不过对于invokedynamic,上述规则不成立,该指令的目的是动态语言支持,这里动态的意思是指必须等到程序实际运行到这调命令的时候,解析动作才能进行,其他的触发解析的条件指令都是静态的,可以在刚刚完成加载阶段,还没有开始执行代码时就提前进行解析。
  解析动作主要针对接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用,分别对应方法池的CONSTANT_CLass_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info、CONSTANT_Dynamic_info和CONSTANT_InvokeDynamic 8种常量类型。

  • 类和接口的解析,主要包含下面三个步骤:

    1. 如果不是数组类型,那么虚拟机将会把符号引用的全限定名传递给所在类的加载器去加载这个类。在加载的过程中由于元数据验证、字节码验证的需要,有可能触发其他相关类的加载动作。一旦这个类加载过程出现任何异常,解析过程就宣告失败。
    2. 如果是数组,并且数组元素类型为对象,也就是描述符类似“[Ljava/lang/Integer”的形式,将会按照第一点的规则加载数组类型。接着虚拟机会生成代表该数组维度和元素的数组对象。
    3. 如果上面两部没有任何异常,那么符号引用已经成为一个有效的类或接口,但是符号解析完还要进行符号引用验证,确认是否有访问权限,如果不具备访问权限,将抛出java.lang.IllegalAccessError异常。
  • 字段解析,解析一个未解析过得字段符号引用,首先将会对字段表内class_index项中索引的CONATANT_Class_info符号引用进行解析,也就是字段所属的类或接口。如果字段所属的类或接口解析失败,会导致字段符号引用解析的失败。解析成功之后,还要对字段所在类或接口后续字段搜索:

    1. 如果类或接口包含了简单名称和字段描述符与目标相匹配的字段,返回这个字段的直接引用,查找结束。
    2. 否则,如果是类实现了父接口或接口有父接口,将会按照继承关系从下向上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
    3. 否则,如果不是java.lang.Object的话,将会按照继承关系从下向上递归搜索其父类,如果在父类中包含了简单名称和字段描述符与目标比配的字段,则返回这个字段的直接引用,查找结束。
    4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。
  • 方法解析,与字段解析一样,也需要解析出方法的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,将会按照下面方式进行查找:

    1. 由于Class文件格式中类中的方法和接口中的方法符号引用的常量类型定义是分开的,如果在类中方法中发现class_index中的索引代表的是一个接口,那就直接抛出java.lang.IncompatibleClassChangeError异常。
    2. 若果通过了第一步,在类中查找是否有简单名称和描述符都与相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
    3. 否则,在父类中递归查找是否有简单名称和描述符都与目标匹配的方法,如果有则直接返回这个方法的直接引用,查找结束。
    4. 否则,在类实现的接口列表及他们父类接口之中递归查找是否有简单名称和描述符都与目标匹配的方法,如果存在匹配方法,说明是一个抽象类,这个时候查找结束,抛出java.lang.AbstractMethodError异常。
    5. 否则查找失败,抛出java.lang.NoSuchMethodError异常。

    查找成功之后还要对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出java.lang.IllegalAccessError异常。

  • 接口方法解析,也需要解析接口方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,按照下面步骤进行方法查找:

    1. 与类解析相反,如果接口方法表中发现class_index中的索引是个类而不是一个接口,那么直接抛出java.lang.IncompatibleClassChangeError异常。
    2. 否则,在接口中查找是否有简单名称和描述符都与目标相匹配的方法,如果有直接放回方法的直接引用,查找结束。
    3. 否则,在接口的父类中递归查找,直到java.lang.Object类(接口方法查找的方位也包含Object类中的方法)为止,看是否有简单名称与描述符都与目标匹配的方法,如果有直接返回这个方法的直接引用,查找结束。
    4. 对于规则3,由于java接口允许多继承,如果不同父类接口中存在多个简单名称与描述符都与目标匹配的方法,那么将会从这多个方法中返回其中一个并结束查找。
    5. 否则,查找失败,抛出java.lang.NoSuchMethodError异常。

5. 初始化

在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。也可以从另一种更直接的方式来表达:初始化阶段就是执行类构造器()方法的过程。是Javac编译器自动生成。

  • 是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定,静态语句块只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,在前面的静态语句块可以赋值,但是不能访问。
  • 方法与类构造函数(即在虚拟机视角中的实例构造器方法)不同,它不需要显示的调用父类构造器,Java虚拟机会保证子类的方法执行前,父类的方法已经执行完毕。因此Java虚拟机中第一个执行的方法类型可定是java.lang.Object。
  • 由于父类的方法先执行,也就意味着弗雷德静态语句块要先于子类的变量赋值操作。
  • 对于类和接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个方法生成()方法。
  • 接口中不能使用静态语句块,但仍然有变量初始化赋值操作,因此接口与类一样都会生成方法,因此只有当父类接口中定义的变量被使用时,父接口才会被初始化。此外,接口的实现类在初始化时也一样不会执行接口的()方法。
  • Java虚拟机必须保证一个类的()方法在多线程环境下被正确的加锁同步,如果多线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,直到活动线程执行完毕()方法。如果一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞,实际应用中这种阻塞往往很隐蔽。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java类加载机制是指将类的字节码文件加载到内存中,并在运行时将其转换为可执行的代码的过程。Java类加载机制遵循了一定的规则和顺序,可以分为以下几个步骤: 1. 加载:类加载的第一步是加载,即将类的字节码文件加载到内存中。Java的类加载器负责从文件系统、网络或其他来源加载类的字节码文件。加载过程中会进行词法和语法的验证,确保字节码文件的正确性。 2. 链接:类加载的第二步是链接,即将已经加载的类与其他类或者符号进行关联。链接分为三个阶段: - 验证:验证阶段确保类的字节码文件符合Java虚拟机规范,包括检查文件格式、语义验证等。 - 准备:准备阶段为静态变量分配内存空间,并设置默认初始值。 - 解析:解析阶段将符号引用转换为直接引用,例如将类或者方法的符号引用解析为对应的内存地址。 3. 初始化:初始化是类加载的最后一步,在此步骤中会执行类的初始化代码,对静态变量进行赋值和执行静态代码块。类的初始化是在首次使用该类时触发的,或者通过反射方式调用`Class.forName()`方法来强制初始化。 Java类加载机制是动态的,可以根据需要加载和卸载类,它还支持类的继承、接口实现、内部类等特性。类加载机制Java语言的重要特性之一,它为Java提供了强大的动态性和灵活性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值