前言
什么是类加载?
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型。
加载什么?
前面的定义已经讲了是加载描述类的数据,也就是Class文件,关于Class文件,我在《深入解析Class类文件的结构》一文中进行了分析。
谁来加载?
加载描述类的类文件的二进制流是由类加载器完成的,已有的三种类加载和自定义的类加载器组成了类加载器子系统,关于类加载器,下文会详细讲述。
怎么加载?
这就是本文的重点,类加载机制中的类加载流程。
可以通过下图整体上看一下类加载在JVM体系中的位置
类的生命周期
类的生命周期共有7个阶段,分别如下图:
前5个阶段属于类加载流程的范围,其中验证、准备、解析又被称为连接,类加载的5个阶段并不是按照顺序依次完成的,除了解析可能会在初始化之后开始,其他的几个阶段的开始顺序是确定的,但结束顺序不一定,可能会交叉着进行,加载还没完成,连接可能已经开始。
类加载流程
类加载分为5个过程,分别是加载、验证、准备、解析、初始化,下面分别对这几个过程进行讲述,尽量简短明了。
加载
"加载"是"类加载"流程的一个阶段
加载阶段主要干的3件事:
- 通过一个类的全限定名获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class实例,作为访问入口
在这三件事里,开发人员能干预的是第一件事,我们可以使用系统的三个类加载器去加载我们想要加载的类文件,也可以自定义类加载器去获取二进制字节流。
定义类的二进制字节流不一定是经过编译后存储在磁盘上的.class文件,有可能是以下来源:
- 从ZIP包中读取,如:JAR、EAR、WAR
- 从网络中获取,如:Applet
- 运行时计算生成,如:动态代理技术
- 由其他文件生成,如:JSP文件生成.class
- 从数据库中读取,中间件服务器,如:SAP Netweaver
Hotspot虚拟机中,Class实例不是在堆上分配空间,而是存放在方法区中,这个实例在代码中可以轻松的获取到,并通过它可以获取代表某个类的各种数据结构。
验证
验证是对输入的字节流进行检查的过程
为什么要有验证这个过程呢?就是因为加载的对象:描述类的二进制字节流,来源广泛,不得不防止它被小人利用,损害虚拟机的正常运行,导致崩溃。所以总共有四个验证过程,分别如下图:
- 文件格式验证
这个阶段直接操作字节流,后面的三个阶段是基于方法区的存储结构,这个阶段主要是验证文件本身的字节码是不是符合规范,目的是保证输入的字节流可以被正确的存储在方法区内。上图中的四个检查项只是其中的一小部分,真正的验证点还有很多。 - 元数据验证
这个阶段主要是验证类的元数据信息是否符合Java语言规范,比如检查是否有父类,除了Objec,其他类都应该要有父类,否则就不符合规范了;被final修饰的不允许被继承。 - 字节码验证
这个阶段主要是对类的方法体进行验证,保证类方法的运行不会对虚拟机造成危害。这是4个验证里最复杂的一个,因为要通过数据流和控制流的分析,确定程序语义是合法的、符合逻辑的。 - 符号引用验证
上面三个阶段是对类本身进行验证,而符号引用验证阶段主要是对类以外的信息进行验证,后面会讲到解析是将符号引用替换成直接引用,所以这里验证的目的是确保符号引用是正确的,确保后面的解析过程能顺利的进行。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段
注意这里是为类变量分配内存,而且是分配在方法区中,实例变量是后面随着实例一起分配在堆上的。
设置初始值也不是代码里赋的值,而是各个数据类型规定的零值,比如基础类型是相应类型不同字节长度的0,引用类型是null。
不是每个类变量都是设置为零值,被final修饰的常量,因为在编译期带有一个ConstantValue属性,属性值则是该常量在代码里赋的值,这个值在准备阶段前就已经确定了,所以在准备阶段设置值的时候,直接取的ConstantValue给类常量。
下面的例子可以很好的了解准备阶段,准备阶段过后,a、b、c分别是多少?
public class Test {
public static int a;
public static int b = 1;
public static final int c = 2;
public void say(){
Syst