您的位置:首页 > 其它

JVM学习笔记(六)-类加载过程

2019-06-12 19:00 99 查看

类加载总体流程

先上图:

类加载总体流程图

  其中,大的步骤分为加载、连接、初始化、使用和卸载,连接包括验证、准备、解析。这里有一点需要注意的地方,图中各阶段的执行顺序问题。

各阶段的执行顺序问题

  图中只有加载、验证、准备、初始化是按部就班执行的,解析阶段不一定是按图中顺序执行。另外,前面所谓的按部就班,并不是严格地说上一阶段执行完了下个阶段才开始执行,也就是说阶段之间会有时间上的交叉。比如,只有在验证阶段的文件格式验证通过后,class文件才能完成加载阶段,然而文件格式验证又只是验证阶段的一小部分。

加载

  加载阶段是第一个阶段。该阶段主要完成三件事情

1.通过类的全限定名获取对应的类的二进制字节流
2.将字节流中的内容加载到JVM运行时数据区存储
3.生成java.lang.Class对象

  关于二进制字节流,在JVM内存区域中已经写过,可以是编译得到的,可以是jar包war包,可以是代理得到,甚至可以自己手写一个。
  当获取了字节流后,就将里面的内容存储在方法区中。
  在成功将class文件内容加载到JVM中后,生成java.lang.Class对象,在Hotspot虚拟机实现中,这个对象在方法区中而不是堆中。

验证

  验证阶段共分为四个部分:

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

文件格式验证

  主要是形式上的检查,就像编译器编译一样,检查整个class文件的内容格式是不是按照class文件格式规定来的。比如说:

  1. 开头是不是“CAFEBABE”
  2. 主版本号次版本号
  3. 常量池的类型有没有问题,你不能自己定义一个不符合规定的类型

  此外,这个阶段有一个特殊的地方就是前面提到的,它是和加载阶段交叉的。只有class文件通过了文件格式验证后,才能加载到方法区中,所以在文件格式验证后的阶段操作的对象都是方法区中存放着的了,只有这一阶段操作的是字节流。

元数据验证

  这一阶段也很好理解,在class文件结构中,常常将内容划分为Code属性和元数据。Code属性指的是方法中的代码块,存放最大操作数栈,最大局部变量表slot数,字节码指令等数据。而元数据就是指的常量池中数据、字段表数据、方法表数据(除Code属性)、类索引、父类索引、接口索引等。所以这一阶段的验证工作肯定不会涉及到代码运行逻辑(我是这么理解记忆的)。比如:

  1. 是否有父类的信息(除java.lang.Object外)
  2. 是否继承了一个final类
  3. 如果这个类不是抽象类,是否实现了父类的所有抽象方法

字节码验证

  上一阶段验证元数据,这一阶段当然要看下Code属性表啦。看名字就知道一定是对字节码指令之类的进行验证。个人认为这样更好记忆:是对代码中数据流和控制流的语义分析。比如:

  1. 指令想要取出一个long型数据,然而操作数栈此时存放的是int型数据,这肯定不行。
  2. 保证跳转指令不会跳转到方法体以外的字节码指令上

  这一阶段还涉及到一个stackMapTable的属性,该属性在Code属性表中,是为了提高验证效率的。它会将那些根据控制流拆分的代码块的本地变量表和操作数栈的初始状态记录下来,更方便判断。

符号引用验证

  这一阶段的验证思路是:你引用我,你找得到我吗?你有权限吗?比如:

  1. 根据符号引用的类全限定名是否找得到这个类
  2. 符号引用对应的类的访问权限是否允许你访问
  3. 符号引用对应的字段的访问权限是否允许。。。
  4. 符号引用对应的方法是否允许。。

没啥好说的,挺简单。

准备

  准备阶段是对类变量分配内存并初始化为相应的零值的。
比如,在类中定义属性:

public static int test1 = 123;//类变量
public static final int test2 = 123; //常量(但也是类变量)
public int test3 = 123; //实例变量

在准备阶段中:
类变量与实例变量的区别

  准备阶段只针对类变量,因此,test1、test2是会分配内存的,而test3此时不会分配内存。

类变量有final与没final的区别

同样是static变量
  有final,由于强制在定义时赋值,所以会有ConstantValue属性,在准备阶段,不仅会分配内存,还会将ConstantValue属性的值赋值给该变量。
  没有final,则在准备阶段只会将对应的内存初始化为零值。其中特殊的几个类型的零值:char为空格即‘\u0000’,boolean为false

解析

  解析阶段将符号引用转换为直接引用。
  符号引用在解析前并没有指向对应的内存,而直接引用指向运行时分配的内存,这个内存存的可以是直接地址也可以是间接地址。
  既然是符号引用,就包含常量池中除字面量外的七种符号引用类型,这里介绍四种:

1.类和接口的解析
2.字段的解析
3.类方法的解析
4.接口方法的解析

类和接口的解析

在类C中有一个未解析的类D

1.当D不为数组时,通过D的全限定名使用C的类加载器去加载D,在此过程中,可能由于元数据验证的需要,还会加载D的父类和接口。发生异常则失败
2.当D为数组时,且数组元素为类N的对象,则会加载N,进行步骤1,然后JVM生成数组对象
3.符号引用验证
只要有一个过程出现异常,则失败。

字段的解析

  在类C中有一个未解析的字段N,该方法的index指向的CONSTANT_CLASS_info对应的类是D

  1. 对D进行类和接口的解析
  2. 在D中查找是否有简单名称和描述符相符的字段,有则返回
  3. 没有就从父类逐级往上找(D非Object类)
  4. 没有就从接口逐级往上找
  5. 没有就失败
  6. 有就符号引用验证,
  7. 任意位置发生异常就解析失败

类方法的解析

  在类C中有一个未解析的方法N,该方法的index指向的CONSTANT_CLASS_info对应的类或接口是D

  1. 若D为接口,抛异常
  2. 对D进行类和接口的解析
  3. 在D中查找是否有简单名称和描述符相符的方法,有则返回
  4. 没有就从父类逐级往上找(D非Object类)
  5. 没有就从接口往上找
  6. 没有就失败
  7. 有就符号引用验证
  8. 任意位置发生异常则解析失败

接口方法的解析

  在类C中有一个未解析的方法N,该方法的index指向的CONSTANT_CLASS_info对应的类或接口是D

  1. 若D为类,抛异常
  2. 对D进行类和接口的解析
  3. 在D中查找是否有简单名称和描述符相符的方法,有则返回
  4. 没有就从父接口往上找
  5. 没有就失败
  6. 有就符号引用验证
  7. 任意位置发生异常则解析失败

初始化

  初始化可以理解为执行<clinit>方法的过程,该方法的内容是将类中的静态块和类变量(除static final修饰外)的显式定义赋值语句。其中静态块和赋值语句的执行顺序和源代码中的顺序一样。静态块可以给出现在后面的类变量赋值,但是无法访问后面的类变量。

静态块无法访问后面的类变量

  可以看到在打印的位置编译报错。
  下面通过javap查看class文件内容,代码改为下面的静态块+显式赋值

package classloadtest;

public class ClinitTest {

static{
a=1;
}

public static int a;
public static int b = 2;

}
Classfile /F:/workspaceforjava/privateTest/src/classloadtest/ClinitTest.class
Last modified 2019-6-12; size 318 bytes
MD5 checksum 4a528ec47fb30ec5d57271bbb7b4d1db
Compiled from "ClinitTest.java"
public class classloadtest.ClinitTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref          #5.#16         // java/lang/Object."<init>":()V
#2 = Fieldref           #4.#17         // classloadtest/ClinitTest.a:I
#3 = Fieldref           #4.#18         // classloadtest/ClinitTest.b:I
#4 = Class              #19            // classloadtest/ClinitTest
#5 = Class              #20            // java/lang/Object
#6 = Utf8               a
#7 = Utf8               I
#8 = Utf8               b
#9 = Utf8               <init>
#10 = Utf8               ()V
#11 = Utf8               Code
#12 = Utf8               LineNumberTable
#13 = Utf8               <clinit>
#14 = Utf8               SourceFile
#15 = Utf8               ClinitTest.java
#16 = NameAndType        #9:#10         // "<init>":()V
#17 = NameAndType        #6:#7          // a:I
#18 = NameAndType        #8:#7          // b:I
#19 = Utf8               classloadtest/ClinitTest
#20 = Utf8               java/lang/Object
{
public static int a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC

public static int b;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC

public classloadtest.ClinitTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<init>
":()V
4: return
LineNumberTable:
line 3: 0

static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: iconst_1
1: putstatic     #2                  // Field a:I
4: iconst_2
5: putstatic     #3                  // Field b:I
8: return
LineNumberTable:
line 6: 0
line 10: 4
}
SourceFile: "ClinitTest.java"

  可以看到,最后是将public static int b= 2放入了静态块中一起赋值。
除此之外,初始化有一些需要注意的地方:

  1. 初始化和准备阶段不同的是,初始化将类变量按照程序员的意愿进行赋值
  2. 接口和方法中都可以有<clinit>方法,但都不是必须有的
  3. 多个线程同时初始化时,JVM会进行同步、加锁,一个线程执行<clinit>,其他线程阻塞等待。

总结

  本篇文章复习了类的加载过程,详细列出了各个阶段的工作和注意事项。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: