JAVA虚拟机(四)-图解类加载机制

版权声明:欢迎转载,标明出处 https://blog.csdn.net/mz4138/article/details/82826466


在学习本节之前,务必了解类文件结构

类加载 是什么,从哪里来到哪里去,什么时候产生,是什么过程?

本节针对普通类(排除数组,排除JDK7+支持动态语言的特性相关)的加载过程

基本理解

定义:

一个比较简单的定义如下:

根据类的全限定名(java.lang.Integer)找到类对象(Class对象),然后将它加载到方法区中

过程

类加载基本流程

类的使用有7个阶段,加载具有5个阶段,我们主要描述的也是这5个阶段

图中按难易程度标注了五个过程

  • 简单: 绿色
  • 一般: 浅红
  • 复杂: 红色,深红色

虚拟机并没有规定类加载的具体时机,只对解析初始化进行了明确的规定.

类加载时机规定

初始化的规定

  1. 先初始化main方法所在的类
  2. 父类未初始化时,先初始化父类
  3. 遇到 new创建对象new Object(),putstatic为静态字段赋值,getstatic获取静态字段的值,invokestatic执行静态方法字节码时
  4. 反射调用类时
  5. 使用JDK7的动态语言支持的时候. MethodHandler的解析结果为 putstatic,getstatic,invokestatic时 (不管它,反正也不用)

解析的规定

在遇到下列字节码之前进行解析:

  • anewarray 创建数组(元素是引用类型)
  • checkcast 对象强制转换
  • getfield 获取字段的值
  • getstatic 获取静态字段的值
  • instanceof instanceof 判断
  • invokedynamic 动态调用点限定符(目前java不会生成,为了支持其他语言用的)
  • invokeinterface 执行接口方法
  • invokespecial 执行私有方法,构造方法
  • invokestatic 执行静态方法
  • invokevirtual 执行虚方法
  • ldc 将int、float或String型常量值从常量池中推送至栈顶
  • ldc_w 将int、float或String型常量值从常量池中推送至栈顶(宽索引)
  • multianewarray 创建多维数组
  • new 创建对象
  • putfield 设置字段的值
  • putstatic 设置静态字段的值

顺序说明

在类加载过程中 加载->验证->准备->初始化 的开始顺序是确定的。
解析却不一定,由虚拟机实现来决定在初始化之前或者之后执行

可以从字节码规定中发现,以下指令是初始化和解析重合的。
所以笔者认为:在遇到以下4个指令的情况下,由虚拟机的具体实现去决定初始化解析谁先谁后

  • new
  • putstatic
  • getstatic
  • invokestatic

图形说明

  • 灰色 无关
  • 红色 顺序不确定
  • 绿色 执行解析

解析字节码规定

类加载五阶段

类加载的触发有3种情况

  1. 虚拟机启动加载(不管)
  2. 由解析阶段触发 (加载,验证,准备 在解析之前)
  3. 由初始化阶段触发,(加载,验证,准备 在初始化之前)

加载

  1. 从类的全限定名(java.lang.Integer)获得二进制字节流(byte[])
  2. 将类静态存储结构(CONSTANT_Class_info)转换为方法区运行时存储结构(虚拟机自己定义结构,没有规范)
  3. 生成Class对象,作为类的数据的访问接口

加载是一个相对简单的流程,也是程序员最可控的流程,可以由程序员自行决定如何通过类的全限定名获取二进制字节流

  • 运行时生成(动态代理)
  • 由文件生成(jsp)
  • 网络中获取(Applet)
  • 压缩文件中获取(war,jar)
    类加载图解

验证

验证是比较复杂的一个阶段,与多个阶段都会交叉执行。 (可以用-Xverify:none 关闭验证)

  1. 文件格式验证
  2. 元数据验证
  3. 字节码验证
  4. 符号引用验证

这里简单的介绍一下各个验证分别是验证什么内容。 主要说明与哪些阶段交叉,什么时候交叉

文件格式验证

  • 文件头是不是0xCAFEBABE
  • 主次版本是否在当前版本处理范围内
  • 是否有不被支持的常量类型

加载阶段格式转换并存储时进行验证,验证通过后,才会在方法区存储

这个阶段是对 二进制字节流验证。后面的都是对静态存储结构(StaticClass)进行验证

元数据验证

  • 是否有父类
  • 是否继承了final类
  • 是否实现了所有抽象方法

在文件格式验证之后

字节码验证

  • 不跳转到方法体外
  • 操作栈的访问正确性(存int按long去访问)

在元数据验证之后

符号引用验证

  • 通过全限定名是否能找到类
  • 符号引用中类、字段、方法的访问性(private,protected,public,default)是否可以被当前类访问

在解析阶段中发生

验证小结

省略了加载阶段中,从全限定名获得二进制字节流的过程。

加入了4个验证阶段的发生的时间
验证阶段说明

准备

这是最简单的一个阶段,只为静态字段赋初始值

非final

静态字段默认初始值:

类型 默认值 类型 默认值
byte 0 short 0
char 0 int 0
long 0 double 0
float 0 boolean false
refrence null

例如下面代码的初始值为0

private static int number = 3;

final静态字段

初始值就是定义的值

例如下面代码的初始值就不是0,而是3

private static final int number = 3;

解析

解析的定义: 将常量池中符号引用替换成直接引用

  • 符号引用: 以一组符号来描述所引用的目标,符号引用的字面量明确规定在class文件格式中(目标不一定在内存)
  • 直接引用: 能直接或间接定位到目标的指针(目标已在内存)

下图为4种解析的说明

解析定义图

解析针对7种符号引用进行,这里只说明4种(与java有关)。剩余的3种与JDK7新增的动态语言有关

类或接口解析

  • 字段不是数组时: 当前类加载器(ClassLoader)去加载这个类型
  • 是数组时: 当前类加载器(ClassLoader)去加载元素类型,数组类型有JVM的类加载器去加载

字段解析

  1. 在当前类查找字段
  2. 在接口列表和接口的父接口查找
  3. 在父类中查找

类方法解析

  1. 查找当前类
  2. 查找父类
  3. 查找父接口,找到则抛出异常(java.lang.AbstractMethodError)
  4. 抛出异常(java.lang.NoSuchMethodError)

接口方法解析

  1. 当前接口查找
  2. 父接口查找
  3. 抛出异常(java.lang.NoSuchMethodError)

初始化

初始化阶段做了以下两件事情

  1. 合并静态字段、静态块生成类构造器方法
  2. 执行类构造器方法

执行类构造器()方法(非实例构造)

类构造器是什么?

类构造器由编译器收集静态字段静态块合并产生的。执行顺序和源码顺序一致

例如:

    private static int a = 1;
    static{
        int ma = 2;
    }
    private static int b = 2;

合并后的类构造器内容

private <clinit>(){
    int a = 1;
    int ma = 2;
    int b = 2;
}

说明

本文的内容全部来自于: 《深入理解Java虚拟机》。

猜你喜欢

转载自blog.csdn.net/mz4138/article/details/82826466