目录
1、类加载过程概述
2、加载
3、连接
3.1 验证
3.1.1 文件格式验证
3.1.2 元数据验证
3.1.3 字节码验证
3.1.4 符号引用验证
3.2 准备
3.3 解析
4、初始化
1、类加载过程概述
想必大家一般在网上看类加载过程的资料时,通常资料只会将类加载过程概括成以下几点:
- 加载
- 连接(验证、准备、解析)
- 初始化
不可否认类加载的过程确实如上,但是大家知道里面的具体细节是怎么样的嘛?别急,本文接下来将会对以上过程一一进行详细的说明。
以上几个阶段是按顺序依次“开始”的。注意!这里说的是“按顺序开始”,而不是“依次、串行运行”。比如:可能加载的过程中,连接阶段已经开始,二者交叉运行。
2、加载
“加载”阶段是整个“类加载”过程中的第一阶段(大家注意区分二者),在类加载阶段,Java虚拟机主要完成以下三件事情:
- 通过类的全限定名来获取该类的二进制字节流。
- 将这个字节流所代表的的静态存储结构转换成方法区运行时的数据结构。
- 在堆内存中生成这个类的java.lang.Class对象(方法区中的klass类是C++的一个类,里面保存了java类的常量池、方法、属性等信息,但是我们无法直接访问,因此需要创建一个Class对象作为方法区数据访问的入口)。
大家可以发现,加载过程中“获取”类的二进制字节流,我并没有强调是从哪里获取,《Java虚拟机规范》中也并没有要求从哪获取,如何获取。正是因为没有要求,这也为Java后来加载类玩出各式各样的花样提供了基础。比如获取类二进制字节流的途径可以随便举以下例子:
- 从ZIP压缩包中获取。
- 从网络中获取(典型应用是Web Applet)。
- 运行时动态生成(动态代理技术)。
- 从数据库中读取
- 从加密文件中读取。
- .............................(还有好多好多方式,我们就不一一举例了)
3、连接
连接我们通常分为三个过程:验证、准备、解析。
3.1 验证
验证是加载阶段的第一个阶段,通常是保证class文件的字节流中包含的信息是有效的,符合《Java虚拟机规范》的全部约束要求,确保这些信息运行后不会产生危害。
其中,验证阶段主要是对以下四个方面进行验证:①文件格式验证。②元数据验证。③字节码验证。④符号引用验证。
3.1.1 文件格式验证
该阶段主要是为了验证字节流是否符合class文件格式的规范(因为文件格式是可以手动更改的,比如我们随便写了个txt文本,然后将文件后缀更改成 “.class” 结尾,如果不加以验证的话,肯定会出大问题的),并且能被当前版本的虚拟机运行。比如对以下几点进行验证:
- 验证魔数(class文件的前四个字节)是否是0XCAFEBABE。
- 主、次版本号是否在虚拟机能处理的范围内,《Java虚拟机规范》要求虚拟机只能向下兼容,不允许向上兼容,即虚拟机只能运行不大于自己当前版本的class文件。
- 常量池中的常量是否存在不支持的常量类型(通过检查tag标志)。
- Class文件是否存在部分内容的缺失。
- ..........................................
该阶段作用是保证输入的字节流能够正确被解析并且加载于方法区内。该阶段的验证是基于二进制字节流进行的,只有通过该阶段,二进制字节流才能被加载进方法区,因此后面的几个阶段是基于方法区的数据结构进行的。
3.1.2 元数据验证
这一阶段主要是对字节码描述的信息进行语义分析,主要是保证字节码描述的信息符合《Java语言规范》的要求,比如:
- 这个类是否有父类(除了Object类以外所有类应该都有父类)。
- 这个类是否继承了不允许继承的类(比如final关键字修饰的类)
- 这个类如果不是抽象类,是否实现了父类或接口中未实现的方法
- ..........................................
3.1.3 字节码验证
这一阶段主要是通过数据流分析和控制流分析,确定程序语义是合法的。该阶段对方法体进行校验分析,保证该方法运行时候不会做出危害虚拟机的不安全行为。验证点例如:
- 保证类型转换是有效的
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- ..........................................
3.1.4 符号引用验证
这一阶段的校验行为发生在“符号引用”转为“直接引用”的时候,这个转化实际上发生在“连接”的第三阶段“符号解析阶段”。符号引用验证实际上是对类本身之外的各种类信息进行验证。验证点例如:
- 符号引用中的类、字段、方法是否可访问(通过访问符:public,protected,private,package等)
- 能否通过类的全限定名找到对应的类。
- ..........................................
3.2 准备
准备阶段实际上是对类中定义的“类变量”,即静态变量(被static关键字修饰的变量)分配内存并设置初始值的阶段。
对于内存的分配,从概念上来说,JDK7以前是在方法区中给static变量分配内存,JDK8以后是在堆中。
准备阶段的主要工作上述已经描述了,这里还需要特别强调:一般情况下,这一阶段仅为类变量分配内存和初始化默认值。因此!实例变量(没被static关键字修饰的变量)是不会在该阶段分配内存和初始化默认值的!
这里分配内存大家肯定都能理解,但是大家可能会有个疑惑:“什么叫初始化默认值?默认值是什么?”
默认值实际上就是我们在定义一个变量时,不设置值时,Java对于这种没设置值的变量赋予了一个“初始值”。这里我们给出基本类型的默认值(也可以叫初始化值、零值):
boolean false
char '/uoooo'(null)
byte (byte)0
short (short)0
int 0
long 0L
float 0.0f
double 0.0d
1、int类型定义的数组 默认是0
2、String类型定义的数组 默认值是null
3、char类型定义的数组 默认值是0对应的字符
4、double类型定义的数组 默认值是0.0
5、float类型定义的数组 默认值是0.0
比如我们以下代码,对于cug这个int类型的类变量,在这个阶段过后的值就是0,而不是2023:
public static int cug = 2023;
当然了,大家再看看我们上面说的那句话提到了“一般情况”,言外之意就是还有“特殊情况”,那么特殊情况是什么呢?那就是被“final”关键字修饰的类变量。由于被“final”关键字修饰的变量都是常量,是不可变的。对于这种类变量会在字段属性表中存在“ConstantValue”属性,那么此时在准备阶段便不是按零值来初始化,而是设置成“ConstantValue”属性所指定的值,例如以下代码在这个阶段后的值是2023,即我们指定的值,而不是零值。
public static final int cug = 2023;
3.3 解析
解析阶段实际上就是将符号引用转为直接引用的过程。
这个阶段一般来说比较复杂,本文不做过多描述,大家有兴趣的可以去看看周志明老师的《深入理解Java虚拟机》,里面讲的非常细节!!!强力推荐!!!
4、初始化
初始化阶段是类加载过程的最后一个阶段。在前面的准备过程中,我们已经为“一般情况”的类变量(即被static关键字修饰且不被final关键字修饰的变量)进行了内存的分配和设置零值。那么这个阶段大家肯定就能猜到要做什么了:“根据我们程序写好的代码进行其他变量的内存分配、设置我们指定的值”。
初始化阶段实际上就是执行类构造器<clinit>方法的过程。这里的<clinit>方法并不是我们在代码中直接编写的方法,而是Javac编译器自动生成的产物。
<clinit>方法主要是编译器自动收集类中所有类变量的赋值操作和静态代码块(static{ })中的语句合并而成,合并后的先后顺序是按我们程序中编写的先后顺序来决定。
对于<clinit>方法,有以下几个需要注意的地方:
- 对于一个类来说,Java虚拟机会保证调用该类的<clinit>方法之前,其父类的<clinit>方法已经被调用。(因此可以知道Object类的<clinit>方法肯定是第一个被调用的,因为它是所有类的父类)。
- <clinit>方法并不是必须生成的,如果一个类中没有静态代码块和类变量的赋值操作,那么不会生成<clinit>方法。
- 接口虽然没有静态代码块,但可能会有变量的初始化赋值操作,因此也会生成对应的<clinit>方法。但是接口和类的<clinit>方法不同的是:接口不要求在执行<clinit>方法前,其父接口的<clinit>方法已经执行,仅仅是用到了父接口的变量才会执行父接口的<clinit>方法。
- 为了保证<clinit>方法在多线程环境下的线程安全和仅被执行一次,因此<clinit>方法会被加锁进行同步。