JVM——深入理解虚拟机类加载机制

概述

一个类从进入虚拟机到卸载主要分为加载、验证、准备、解析、初始化、使用、卸载几个阶段,其中验证、准备、解析统称为连接

 

类加载的时机(类型表示类或者接口)

类加载的过程其实并不是按部就班的进行了,虚拟机规定以下六种情况必须立即进行“初始化”(加载、连接等在这之后进行)

  1. 只有遇到new、getstatic、pustatic、invokestatic这个几个指令时,如果这个类型没有过初始化,就会进行这个类的初始化。这几个指令会在如下几种情况中出现
  2. 当new对象的时候,会对对象所属的类进行初始化
  3. 调用类中静态变量时(被final修饰的常量除外)
  4. 调用类中静态方法时
  5. 当使用了一个类的反射的时候,如果该类没有初始化过,就必须先触发该类的初始化
  6. 当调用一个类时,如果这个类的父类没有初始化过,就会先初始化它的父类
  7. 如果main函数所在的类没有被初始化会首先初始化这个类
  8. 如果一个接口使用了deault关键字,且这个接口的子类需要初始化,就需要在初始化子类前先初始化这个接口

另外有几点注意事项

  • 当一个类中定义静态字段、且又被子类继承时,通过子类调用父类的静态字段是不会触发子类的初始化的,只会触发父类的初始化
  • 通过数组的定义来引用类(意思就是数组元素是该类),不会触发类的初始化
  • 类的子类被调用会触发父类的初始化,但是接口并不会因为子类接口被调用而被初始化(指父类接口),只有用到这个接口时才会触发此接口的初始化

类加载的过程

加载

加载是类加载的一个阶段,这个阶段java虚拟机主要负责以下三件事情

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

简单总结这三点:获取类的二进制字节流并储存,并在内存中生成一个Class作为访问该类的入口

相对于类加载过程的其他阶段,非数组类型的加载阶段是开发人员可控性最强的阶段,可以使用java虚拟机内置的引导加载器来完成,也可以使用用户自定义的类加载器来完成。

而对于数据类型,情况则有所不同,数组本身不由类加载器创建,而是可以由java虚拟机在内存中动态构造出来的,但是数组的元素类型(指数组去掉所有维度的类型)依旧是需要通过类加载器来完成加载的

  1. 如果数组的组件类型(指数组去掉一个维度的类型)是引用类型,则会递归采用加载过程来加载这个组件类型
  2. 如果数组的组件类型不是引用类型(如int[]数组组件类型是int),java虚拟机会把该数组标记为与引导类加载器相关联
  3. 数组的访问类型与它的组件类型的可访问性一致,如果组件类型不是引用类型,则会把数组类的可访问性默认为public

验证

这个阶段主要确保Class文件的字节流包含的信息符合 Java虚拟机规范,主要包括以下几个阶段

  1. 文件格式验证 :这个阶段主要检查Class文件格式的规范,比如是否以魔数开头、主次版本号是否可被接受等等
  2. 元数据验证:这个阶段主要是对象字节码描述的语义分析,比如这个类是否有父类,这个类是否继承了被final修饰的类等等,只有通过了这个阶段的验证后 ,这段字节流才被允许进入java虚拟机的方法区中进行存储
  3. 字节码验证:主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。对元数据校验完毕后,这阶段就要对类的方法体进行校验分析
  4. 符号引用验证:主要负责符号引用的正确性,比如符号引用中通过字符串描述的全限定名能否找到对应的类,以及符号引用类的可访问性是否可别当前类访问

准备

准备阶段是正式为类中定义的变量分配内存并设置初始值(默认零值)的阶段

解析

解析阶段是Java虚拟机中符号引用转为直接引用的阶段

符号引用是以一组符号的形式描述所引用的目标,符号可以是任何没有歧义的字面量(符号引用即用(字符串符号的形式)来表示引用)举个例子

 

直接引用则是可以直接指向目标的指针、相对偏移量、或者是一个能够间接定位到到目标的句柄,一般说如果有了直接引用,那引用的目标必定已经在虚拟机中存在

类和接口的解析

假设当前所处的类为D,需要把一个从未解析过的符号N解析为一个类或接口C,则包括以下三个步骤

  1. 如果C不是一个数组类型,则虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类。在加载过程中,由于元数据验证、字节码验证的需要,又可能在C是某一个类的子类的情况下触发父类的其他加载动作,一旦出现了异常,解析过程宣告失败
  2. 如果C是一个数组类型,那就会按照第一点的规则加载C的元素类型,接着由虚拟机生成一个代表该数组维度和元素的数组对象
  3. 如果以上过程没异常,那么C在虚拟机中就已经是一个有效的类和借口了,但在解析完成前还要进行符号引用验证,确定D是否有访问C的权限,如果没有则会报IllegalAccessError异常

字段解析

要解析一个未被解析过的字段符号引用,首先需要解析出字段表的class_index (class_index是指常量池中索引为class_index的常量项),也就是字段所属的类或接口的符号引用,解析成功后,就把这个字段所属的类或接口用C表示

  1. 如果C本身就包含了与目标匹配的字段,就返回这个字段的直接引用
  2. 否则,如果在C实现了接口,就会按照继承关系从下往上的递归搜索各个接口和它的父接口,如果接口中包含于目标匹配的字段,就返回这个字段的直接引用
  3. 否则,如果C不是Object类的话,将会从下往上的搜索它的父类,如果包含于目标匹配的字段,就返回这个字段的直接引用,查找结束
  4. 否则,查找失败,抛出java.long.NoSuchFieldError异常

如果查找过程中进行权限验证发现不具备对字段的访问权限,将抛出IllegalAccessError异常

方法解析

步骤与字段解析的第一个步骤一样,首先需要解析出方法表的class_index也就是方法所属的类或接口的符号引用,如果解析成功同样用C表示这个类

  1. 如果在类的方法表中通过class_index解析出C是个接口的话,直接抛出异常
  2. 在类中查找与目标匹配的方法,如果有就直接返回该方法的直接引用
  3. 否则,在C的父类中递归查找相匹配的方法,有则返回方法的直接引用,查找结束
  4. 否则,在C实现的接口列表及他们的父接口中递归查找匹配的方法,如果存在匹配的方法,说明C是一个抽象类,查找结束,抛出AbstractMethodError异常

最后同样需要进行权限验证

接口方法解析

同样需要解析class_index如果解析成功说明C是一个接口

  1. 与类的方法解析相反,如果在接口方法表章发现class_index的索引C是个类而不是接口,就会抛出异常
  2. 否则,在接口C中查找匹配的方法,如果有则返回该方法的直接引用
  3. 否则,在接口C的父接口中递归查找,直到查找到Object类为止,找到返回直接引用
  4. 对于规则c,由于Java的接口允许多重继承,如果在C的不同父接口中发现了多个匹配的方法,那么将会返回其中一个的直接引用
  5. 否则查找失败

初始化

初始化阶段就是执行类构造器<clinit>()方法的过程,,<clinit>()方法并不是程序员在java代码中编写的方法,而是由javac编译器自动收集类中所有变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是按照语句在源文件中出现的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,静态语句块可以对其赋值但无法访问

  • <clinit>()与构造函数不同,它不需要显式的调用父类构造器,java虚拟机会保证父类的<clinit>()方法在子类的该方法之前执行,所以java中最先执行的一定是Object的<clinit>()方法
  • <clinit>()并不是必须存在的,如果类中没有对变量的赋值,也没有静态语句块,那么编译器可以不生成这个类的<clinit>()方法
  • 接口不能使用静态代码块,但仍有变量初始化的赋值操作,因此接口同样会生成<clinit>()方法,但是执行接口的<clinit>()方法并不需要执行父接口的<clinit()方法,只有父接口被调用时才会执行父接口的<clinit>()方法
  • java虚拟机必须保证一个类的<clinit>()方法被正确的加锁同步,如果有多个线程执行类的初始化,那么一定有一个线程执行该类的<clinit>()方法,其他线程都要阻塞等待,如果在执行一个类<clinit>()方法时有耗时很长的操作,那么就可能造成多个线程堵塞
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值