类加载的五个过程:加载、验证、准备、解析、初始化

类加载的五个过程:加载、验证、准备、解析、初始化_第1张图片

1、加载

所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,(一个Java文件从编码完成到最终执行,一般主要包括两个过程:编译和运行,其中编译就是把我们写好的java文件,通过javac命令编译成字节码,也就是我们常说的.class文件,然后运行则是把编译声称的.class文件交给Java虚拟机(JVM)执行。而我们所说的类加载过程即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程。)并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用

实际上java的每个类被编译成.class文件的时候,java虚拟机(jvm)会自动为这个类生成一个类对象,这个对象保存了这个类的所有信息(成员变量,方法,构造器等),以后这个类要想实例化(也就是创建类的实例或创建类的对象)那么都要以这个class对象为蓝图(或模版)来创建这个类的实例。例如
class c=Class.forName(“com.pojo.User”); c就是User的类对象,而 User u=new User();这个u就是以c为模版创建的,其实就相当于u=c.newInstance(); 这个在java的反射里面讲的比较清楚。

反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法反射

简单来说:加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。


举个通俗点的:
JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

在加载类时,Java 虚拟机必须完成以下3件事情:

  • 通过类的全名,获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  • 创建 java.lang.Class 类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

二进制流的获取方式:

对于类的二进制数据流,虚拟机可以通过多种途径产生或获得。(只要所读取的字节码符合 JVM 规范即可)

  • 虚拟机可能通过文件系统读入一个 Class 后缀的文件(最常见)
  • 读入 jar、zip 等归档数据包,提取类文件
  • 事先存放在数据库中的类的二进制数据
  • 使用类似于 HTTP 之类的协议通过网络进行加载
  • 在运行时生成一段 Class 的二进制信息等

在获取到类的二进制信息后,Java 虚拟机就会处理这些数据,并最终转为一个 java.lang.Class 的实例

类模型与 Class 实例的位置

1、类模型的位置
加载的类在 JVM 中创建相应的类结构,类结构会存储在方法区(JDK 1.8之前:永久代;JDK 1.8之后:元空间)

2、Class 实例的位置
类将 .class 文件加载至元空间后,会在堆中创建一个 java.lang.Class 对象,用来封装类位于方法区内的数据结构,该 Class 对象是在加载类的过程中创建的,每个类都对应有一个 Class 类型的对象

类加载的五个过程:加载、验证、准备、解析、初始化_第2张图片


外部可以通过访问代表Person类的 Class 对象来获取 Person的类数据结构(看上图)

Java 编译成字节码后,要运行需要通过 类加载器,将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 fifield(域) 有:

  • _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 class 暴露给 java 使用
  • _super 即父类
  • _fifields 即成员变量
  • _methods 即方法
  • _constants 即常量池
  • _class_loader 即类加载器
  • _vtable 虚方法表
  • _itable 接口方法表

如果这个类还有 父类 没有加载,先加载父类;

加载和链接可能是交替执行的

  • instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),但 _java_mirror是存储在堆中


2、链接


2.1验证


① 文件格式验证

四个验证过程中,只有格式验证是建立在二进制字节流的基础上的。格式验证就是对文件是否是0xCAFEBABE开头、class文件版本等信息进行验证,确保其符合JVM虚拟机规范。

这一阶段具体可能包括下面这些验证点:

  • 是否以魔数0xCAFEBABE开头。
  • 主、次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
  • 指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。


实际上,第一阶段的验证点还远不止这些,上面这些只是从HotSpot虚拟机源码中摘抄的一小部分内容,该验证阶段的主要目的是保证输入的字节流能正确的解析并存储于方法区之内,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面3个验证阶段全部是基于方法区的存储结果进行的,不会再直接操作字节流。

②元数据验证

元数据验证是对源码语义分析的过程,验证的是子类继承的父类是否是final类;如果这个类的父类是抽象类,是否实现了其父类或接口中要求实现的所有方法;子父类中的字段、方法是否产生冲突等,这个过程把类、字段和方法看做组成类的一个个元数据,然后根据JVM规范,对这些元数据之间的关系进行验证。所以,元数据验证阶段并未深入到方法体内。

这一阶段具体可能包括下面这些验证点:

  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。

③字节码验证

既然元数据验证并未深入到方法体内部,那么到了字节码验证过程,这一步就不可避免了。字节码主要是对方法体内部的代码的前后逻辑、关系的校验,例如:字节码是否执行到了方法体以外、类型转换是否合理等。
这一阶段具体可能包括下面这些验证点:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险和不合法的。

当然,这很复杂。所以,即使是到了如今jdk1.8,也还是无法完全保证字节码验证准确无遗漏的。而且,如果在字节码验证浪费了大量的资源,似乎也有些得不偿失。

④符号引用验证

符号引用的验证其实是发生在符号引用向直接引用转化的过程中,而这一过程发生在解析阶段。

这一阶段具体可能包括下面这些验证点:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段
  • 符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问。

验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过了验证,其后就对程序运行期没有任何影响了

2.2 准备


为 static 变量分配空间,设置默认值

  • static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
  • static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
  • 如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
  • 如果 static 变量是 final 的,但属于引用类型,即 new 对象,那么赋值也会在初始化阶段完成

2.3 解析


解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程

1、符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可

2、直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄

符号引用与虚拟机实现的内存布局无关,直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。

如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行

3、初始化

初始化即调用 < cinit >()V 方法,虚拟机会保证这个类的 【构造方法】的线程安全

发生的时机

会导致 类初始化 的情况

  • main 方法所在的类,总会被首先初始化
  • 首次访问这个类的 静态变量 或 静态方法 时
  • 子类初始化,如果父类还未初始化,会引发
  • 子类访问父类的静态变量,只会触发父类的初始化
  • Class.forName
  • new 会导致初始化

不会导致 类初始化 的情况

  • 访问 类的 static final 静态变量(基本类型和字符型)不会触发初始化
  • 类对象.class 不会触发初始化
  • 创建该类的数组不会触发初始化
  • 类加载的 loadClass 方法
  • Class.forName 的参数2 为 false 时
     

你可能感兴趣的:(JVM,jvm,java)