你的位置:首页 > Java教程

[Java教程]第四章 类加载机制


注:本文主要参考自《深入理解java虚拟机(第二版)》

在查看本文前,先要了解JVM内存结构,见 第一章 JVM内存结构

1、类加载流程

  • 把描述类的数据从xxx.class文件加载到JVM内存
  • 对这些数据进行校验、准备、解析(这三个过程总称为"链接")
  • 对这些数据进行初始化,最终形成可被JVM直接使用的Class对象

注意:

  • 类加载过程是在运行期完成的

 

2、加载

  • 作用:把描述类的数据从xxx.class文件加载到JVM内存
  • 此阶段完成三件事
    • 通过一个类的全限定类名来获取定义此类的二进制字节流
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • 在方法区生成一个java.lang.Class对象,作为方法区内该类的各种数据的访问入口
  • 注意:
    • 类加载的完成是靠全限定类名和类加载器(ClassLoaderA实例)完成,这两个元素也是标识一个已被加载类的标识
    • 接口、类:名称为"全限定类名+ClassLoader实例ID",这种类型的类由所在的ClassLoader负责加载
    • 数组:"[基本类型"或"[L引用类型;"(eg.byte[] bytes = new byte[512];//类名为"[B";Object[] objs = new Object[10];//类名为"[Ljava.lang.Object;")
      • 注意基本类型名称都是取自首字母大写(eg.byte:B),有两个例外boolean:Z;long:J
      • 数组类由JVM直接创建,其数组元素由类加载器加载

 

3、验证

  • 四部分验证
    • 文件格式验证:当通过一个类的全限定类名获取了定义此类的二进制字节流后,检验该字节流是否满足class文件格式(例如:开头是魔数,主次版本号是否是当前虚拟机处理范围之内,即是否在次到主之间的版本,高于主的版本不加载(这就是低版本的jdk无法执行高版本jdk的原因),等等),检验过后,执行"加载"部分的第二件事,将数据放入方法区并存储
      • 格式不符合:java.lang.VerifyError
    • 元数据验证:保证类的元数据信息符合java语言规范(例如:这个类若是普通类,是否实现了其实现接口下的所有类)
      • 关于类的元数据有哪些,查看 第二章 Javac编译原理
    • 验证方法体的字节码指令,确定指令顺序符合逻辑
    • 符号引用验证:
      • 发生在解析阶段:将符号引用转化为直接引用的过程
      • 对符号引用的验证,保证这些符号引用(属性、方法等)存在,并且具备相应的权限(eg.保证private的方法不可被其他类访问)
        • NoSuchMethodError、NoSuchFieldError、IllegalAccessError
  • 注意:
    • 验证阶段可能会穿插在类加载的其他阶段
    • -Xverify:none:关闭大部分的验证操作,这个我们一般不会配置这个参数(即我们一般还是会去做验证的),因为类加载虽然发生运行期,但是大部分的类加载行为是在服务器启动的时候就发生了,实际上不影响我们的程序运行。当然,如果我们手动调用了Class.forName(),this.getClass.getClassLoader().loadClass(),这样就会执行到这句代码的时候加载类,但毕竟是少数。通常我们会使用Class.forName()来引入MySQL驱动,这也就是在maven中引入MySQL驱动包的时候,其scope是runtime的原因。

 

4、准备

  • 作用:为类变量分配内存(这些内存在方法区分配)、设置类变量零值
  • 注意:
    • 为类变量分配内存(为static对象分配内存,这也就是static变量存储在方法区的原理)
    • 类中的实例变量会随着对象实例的创建一起分配在堆中,当然若是基本数据类型的话,会随着对象的创建直接压入操作数栈
    • 为static变量分配零值(默认零值)(eg.static int x = 123;//这时候x = 0,而x = 123是在"初始化"阶段发生的)
    • 为final变量分配初始值(期望值)(eg.final int y = 456;//这时候y = 456)

 

5、解析

  • 作用:符号引用转化为直接引用
  • 符号引用(编译期):存储于class文件的常量池中(见 第三章 类文件结构与javap的使用),只有一些名称,没有实际的地址
  • 直接引用(运行期):在该阶段,会为符号引用分配实际的内存,之后符号引用就转化为了直接引用,存储于运行时常量池
    • 常量池:class文件中的概念
    • 运行时常量池:JVM内存结构中方法区内的一个组成部分
  • 注意
    • 该步骤在"初始化"阶段之前不一定发生,但是一定要在一个符号引用被使用前解析。

 

6、初始化

  • 作用:执行静态块(static{})、初始化(按意愿)static变量(eg.static int x = 123;//"准备"阶段后,x = 0;"初始化"后,x = 123)、执行构造器
  • 发生的时机:
    • new:执行构造器
    • 子类调用了初始化,若父类没有初始化,则父类先要初始化(eg.子类调用了"new 无参构造器",则先要执行父类的"无参构造器")
    • 反射调用了类,若类没有初始化,需要先初始化
    • 虚拟机启动时,包含main()方法的类要初始化
  • 注意:static{}与static变量初始化的发生不一定会发生在服务器启动时(即不一定发生在类加载时),若想要达到容器启动后,就执行一段代码xxx,可以将类实现spring的InitalizingBean,重写该接口下的afterPropertiesSet()方法,将xxx代码写入该方法中。

 

总结:

  • 类加载流程
    • "加载"第一阶段:通过一个类的全限定类名来获取定义此类的二进制字节流
    • "验证"第一阶段:文件格式验证
    • "加载"第二阶段:将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
    • "验证"第二阶段:元数据验证
    • "验证"第三阶段:验证方法体的字节码指令
    • "准备"第一阶段:类变量分配内存
    • "准备"第二阶段:设置类变量零值
    • "验证"第四阶段:符号引用验证(追随于"解析",仅发生在"解析"的时候)
    • "解析":符号引用转化为直接引用(不是必须要在"初始化"之前的步骤的步骤)
    • "初始化":不一定发生在何时,但是一定要在"加载"、"验证"、"准备"之后
  • 关于验证部分,我们可能会怀疑既然javac在"语法分析"和"语义分析"部分已经做了大量的验证,类加载的时候为什么还要进行验证?
    • javac并没有做在类加载部分的所有验证,例如:魔数验证
    • 不是所有的class文件都是由javac产生的,还有第三方的jar,甚至是自己伪造的class