知识脑图

类加载机制 - 图1

类的生命周期

类的生命周期一共分为7个阶段 前五个阶段为加载阶段 其中验证、准备、解析这三个部分统称为连接

  1. 加载
  2. 验证
  3. 准备
  4. 解析
  5. 初始化
  6. 使用
  7. 卸载

    类的加载阶段

加载

在加载阶段,Java虚拟机需要完成以下三件事情

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


扩展知识

在类加载阶段,JVM并没有特别具体要求,留给虚拟机实现Java应用的灵活度都是相当大的,规则一中并没有指明二进制字节流必须从某个Class文件中获取。充满创造力的开发者们在这之上衍生出了一系列举足轻重的技术,例如:

  • 从ZIP压缩包中读取,这很常见,最终成为日后的JAR、EAR、WAR格式基础
  • 从网络中获取
  • 运行时计算生成,这种场景使用的最多的就是动态代理技术
  • 从其他文件生成,典型场景就是JSP
  • 可以从加密文件中获取,防止反编译
  • 等等一系列…

相对于类加载过程中的其他阶段,非数组类型的加载阶段(准确的说是,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的阶段,加载阶段既可以使用Java虚拟机内置的引导类加载器去完成,也可以由用户自定义的类加载器去完成,开发人员通过自定义的类加载器去控制字节流的获取方式(重写一个类加载器的 findClass()loadClass() 方法),实现根据自己的想法来赋予应用程序获取运行代码的动态性

对于一个数组类而言,情况就有所不同。数组类本身并不通过类加载器创建,它是由Java虚拟机在内存中动态构建的,但数组类与类加载器仍然有着很密切的关系,因为数组的元素类型(Element Type,指的是数组去掉所有维度的类型)最终还是要靠类加载器来完成加载。

一个数组类C
如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,那就递归采用加载过程去加载这个组件类型 数组C将被标识在加载改组件类型的类加载器的类名称空间上

如果数组的组件类型不是引用类型(int[]数组的组件类型就是int),Java虚拟机将会把数组C标记为与引导类加载器关联

数组的可访问性与它的组件类型的可访问性一致,如果数组的组件类型不是引用类型,它的数组类的可访问性将默认设置为public,可被所有类和接口访问到

验证

验证作为连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束邀请,保证这些信息被当做代码运行后不会危害虚拟机自身的安全。验证阶段大致可以分为四个阶段:

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证
    文件格式验证是否以魔数0xCAFEBABE开头,常量池中是否有不被支持的常量类型,该阶段主要目的是保证输入的字节流信息能正确的存储到方法区。这个阶段的验证是基于二进制字节流的进行的,只有通过了这个阶段的验证后字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区存储结构进行的不会再直接操作字节流。

第二阶段主要是对类的元数据进行语义检验,保证其不存在与《Java语言规范》定义相悖的元数据信息

准备

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

类变量是被 static 修饰的的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存

ps: 方法区是一个逻辑上的分区 在1.7及之前 HotSpot使用永久代实现方法区,而在1.8之后类变量随着Class对象一起存放在堆中

实例变量不会再此阶段分配内存,它会在对象实例化时伴随对象一起在堆中分配。实例化不是类加载的一个过程,类加载发生在所有实例化之前,并且类加载只会进行一次,而对象实例化可以进行多次。

这里的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为

  1. public static int value = 123;

那变量value在准备阶段过后的初始值为0,而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器 () 方法之中,所以把value赋值为123的动作要等到类初始化阶段才会执行

数据类型 零值 数据类型 零值
int 0 boolean false
long 0l float 0.0f
short (short)0 double 0.0d
char ‘u0000’ reference null
byte (byte)0

Java中所有基本数据类型的零值

上面提到“
通常情况”下初始值为零值,那言外之意是相对的会有一些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue**属性所指定的初始值,假设上面的类变量value的定义修改为:

  1. public static final int value = 123;

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123.

简单概况: 为静态变量(类变量)分配内存并设置默认值

解析

当通过解析阶段后,jvm针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符7类引用进行解析。这个阶段主要任务是将其再常量池中的符号引用替换为直接其在内存中的直接引用。

其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持Java的动态绑定

简单概括:解析类和方法,将常量池中的符号引用转化为直接引用,确保类与类之间相互引用正确性,完成内存结构布局。