总体概括:将.class文件加载到内存,并对数据进行校验,转换解析和初始化,最后形成一个能被虚拟机使用的java类型,这个过程就是类的加载机制
虚拟机的加载机制包括以下几个重要步骤:
1.加载 获取类.class文件的二进制流,在方法区 生成一个代表这个类的java.lang.Class对象,作为这个类的各种数据的访问入口。
2.验证 确保.class文件的字节流中包含的信息符合虚拟机的要求,不会危害虚拟机的自身安全
3.准备 为类变量分配内存并设置类变量初始值(零值)
4.解析 是虚拟机将常量池内的符号引用转换为直接引用的过程
5.初始化 为类变量赋值 执行Clinit方法
先来一张图整体了解一下:
下面开始具体分析
1.加载过程
类的加载主要是由类加载器(ClassLoader)完成,类加载器会在后面的文章中介绍 类加载器工作流程
类的加载主要完成三件事
1)通过类的全限定名来获取定义此类的二进制字节流,并加载到内存(也就是.class文件)
2)将二进制流中的静态存储结构转换为方法区的运行时数据结构
3)在方法区中生成一个代表该类的java.lang.Class对象实例,用来作为访问该类各种数据的外部接口
值得注意的有一下两点:
1.二进制流的获取有多种途径最典型的有从zip中获取,如.jar包,从网络中获取,运算时生成,由其他文件生成如.jsp文件
2.如果加载的是一个非数组类,加载阶段可以直接用系统提供的引导类加载器加载,也可以由一个继承自ClassLoader的自定义加载器加载。
1)如果是数组类,数组本身不通过类加载器构建,而是由java虚拟机直接创建。
2)但是数组元素类型如果是引用类型如String[],依旧会采用类加载器去加载,并且该数组将在加载该元素类型的类加载器的类名称空间上被标识。(一个类必须类加载器一起决定了唯一性)
3)如果数组元素类型不是引用类型如int[],虚拟机会把数组标记为与引导类加载器相关联。
4)如果数组的元素类型不是引用类型,那么数组的可见性将默认为public
2.验证过程
验证的主要目的,是要确保class文件的字节流包含的信息要符合当前虚拟机的要求,并且不会有恶意的代码会危害到虚拟机的安全。
验证过程分为四大块:
1.文件格式的验证 --保证了字节流能真确的解析并存储到方法区,这阶段是基于二进制,自由通过了这一阶段的验证,字节流才会进入内存的方法区中存储。其后面的三个验证阶段都是在方法区的存储结构中进行
2.元数据的验证 (语法检测)
3.字节码的验证 -- 确定程序的语义是否合法,符合逻辑
4.符号引用的验证 -- 可以看做是对类自身以外的信息进行匹配性校验,发生在虚拟机将符号引用转换为直接引用的时候,也就是类加载的 解析阶段。
具体的验证方式与方法相当复杂,此处不一一分析,有兴趣的可以去查阅《java虚拟机规范》一书的class文件结构一章
验证阶段完成之后,二进制流已经在方法区中正确存储,语法语义校验也已经通过,符号引用验证也已经完成。
3.准备过程
准备阶段可以看做是类的系统初始化阶段,也就是为类的 类变量 分配内存空间并设置初始值的过程
注意:此处与后文提到的类变量均是指类的 static变量
通常情况下 初始化类变量也就是为static变量赋 零值 的过程 如:
public static int value =123;在经过准备阶段之后value的值为 0,要到初始化阶段结束之后value才会为123
但是也有特殊情况,那就是如果类字段的字段属性表中存在该属性,那么在准备阶段变量value就会被初始化为该属性指定的值。如:
public static final int value =123; 那么准备阶段之后value的值为123.
也就是说,常量对象会在准备阶段就会赋值
4.解析过程
解析过程是将常量池中的 符号引用 转换为 直接引用 的过程
解析阶段分为四种情况
1)类和接口的解析
假设当前代码所处的类为 D,如果要把一个从未解析过的符号引用 N 解析为一个类或者接口 C 的直接引用,那个它会经历如下几个阶段:
(1)如果 C 不是一个数组类型,那么虚拟机会把代表 N 的全限定名传给 D 的类加载器去加载这个类C,在加载过程中又可能触发其他相关类的加载动作,一旦加载过程出现异常,则解析失败。
(2)如果 C 是一个数组类型,并且数组的元素类型为对象,也就是N的描述符会为[Ljava/lang/Integer的形式,那将会按照上面第一点的加载规则去加载对应的类型,接着由虚拟机生成一个代表这个数组维度和元素的数组对象。
(3)如果上面的步骤没有异常,那么 C 在虚拟机中已经是一个有效的类或者接口了,但解析完成之前还要进行符号引用验证,确定 D 是否具备对 C 的访问权限
2)字段的解析
假设当前字段所在的类或接口已经解析完成,用 C 表示:
(1)如果 C 中本身就包含了简单名称与字段描述都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
(2)否则,如果该类实现了接口,将会按照继承关系从下往上递归搜索各个接口跟父接口,如果接口中包含了简单名称与字段描述都与目标相匹配的字段,则返回这个字段的引用,查找结束。
(3)否则,如果该类不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称与字段描述都与目标相匹配的字段,则返回这个字段的引用,查找结束。
(4) 否则,查找失败,抛出异常
(5) 如果查找成功,还要对字段进访问行权限验证,如果不具备访问权限也将抛出异常
3)类方法的解析
假设当前的类或接口已经解析完成,用 C表示:
(1) 类方法 与 接口方法符号引用的常量类型定义是分开的,如果类方法表中发现class_index中索引的 C 是个接口,直接抛出异常
(2) 如果通过了第一步,在类C中查找是否有简单名称与描述符都与目标相匹配的方法,如果有返回这个方法的直接引用,查找结束。
(3)否则,在类 C 的父类中递归查找是否有简单名称与描述符都与目标相匹配的方法,如果有返回这个方法的直接引用,查找结束。
(4)否则,在类 C 实现的接口以及接口的父类中递归查找否有简单名称与描述符都与目标相匹配的方法,如果有返回这个方法的直接引用,查找结束。
(5) 否则,查找失败,抛出异常
(6) 如果查找成功,还要对方法进行访问权限验证,如果不具备访问权限也将抛出异常
4)接口方法的解析
假设当前的类或接口已经解析完成,用C表示:
(1) 类方法与接口方法符号引用的常量类型定义是分开的,如果接口方法表中发现class_index中索引的C是个类,直接抛出异常
(2) 如果通过了第一步,在接口C中查找是否有简单名称与描述符都与目标相匹配的方法,如果有返回这个方法的直接引用,查找结束。
(3)否则,在接口 C 的父类中递归查找(直到java.lang.Object)是否有简单名称与描述符都与目标相匹配的方法,如果有返回这个方法的直接引用,查找结束。
(4) 否则,查找失败,抛出异常
(5) 由于接口中的方法默认都是public的,所以不存在访问权限校验的问题
解析过程相对复杂,但原理相通,比较好理解
5.初始化过程
初始化过程是类加载阶段的最后一步,在编译器生成.class文件时会在class文件中自动生成一个类初始化方法与对象初始化方法。即Clinit ()【类初始化方法】 与 init()【对象初始化方法】。
类初始化的过程实质上是为 类变量(static) 根据程序员的意愿赋值的过程。(执行Clinit方法的过程)
Clinit方法的定义是:编译器自动收集类中的所有 类变量(static) 的赋值动作和 静态语句块 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它后面的变量可以赋值但不能访问。
Clinit方法的执行要注意以下几点:
1.在执行子类的 clinit 方法时会确保父类的 clinit 方法先执行完毕
2.由于父类的clinit方法先执行,所以父类中的静态块与静态变量要先于子类的赋值操作
3.如果子类与父类中都没有静态变量与静态块,则编译器不会生成clinit方法
4.接口中的clinit方法
(1)接口中不能使用静态块,但是依旧可以有赋值操作,所以接口也有clinit方法
(2)但是接口与类不同的是,接口执行clinit方法时不需要先执行父类的clint方法
(3)只有当接口的 类变量 使用时,父接口才会初始化
(4)接口的实现类 在初始化时一样不会执行接口的clinit方法
5.clinit方法线程安全,在同一个类加载器下,一个类型只会初始化一次!
到此处类的加载过程已经分析完毕!