开门见山 解决问题
类加载是JVM将Java程序运行所需的class文件加载到内存中的的一个过程。class文件内包含了类的各种描述信息。类加载包含加载、验证、准备、解析和初始化五个阶段。
类的生存周期包含以下过程:
类加载的五阶段
1. 加载
JVM在这个阶段完成:
- 根据类的全限定名获取定义此类的二进制字节流。并没有指明要从一个class文件中获取,可以从其他渠道,譬如:zip包、网络、动态生成、数据库、jsp生成的二进制class文件等。
- 将折合字节流代表的静态存储结构转换成方法区域的运行时数据结构。
- 在方法区围着各类生成一个java.lang.Class对象,作为方法区这个类的访问入口。
加载的阶段和Linking阶段的部分如字节码文件格式验证过程是交叉进行的,加载阶段未完成时Linking阶段可能便已经开始,但最初的开始时间还是按照生命周期的。
另外,数组的加载和普通类有些不一样。因为数组本身不通过类加载器加载,是直接通过虚拟机自动生成,但该数组的数组类型依旧需要依靠类加载器来完成加载,其过程遵循以下规则:
- 如果数组的类型是引用类型,则引用类型需要使用递归来进行加载,并且数组需要被加载该数组类型的类加载器的命名空间上进行标识。
- 如果数组的类型不是引用类型,是基本数据类型,Java虚拟机将会把数组标记为与引导类加载器关联。
- 数组的可见性与数组类型的可见性保持一致,如果数组类型是基本类型,则默认可见性为public。
2. 验证
验证是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且(恶意代码)不会对虚拟机的运行产生危害(崩溃)。验证是阶段里持续时间最长的,同时占用了类加载子系统很大一部分资源。验证不是必须的,可以通过 -Xverifynone 来关闭大部分验证动作,这样也会缩短类加载时间。
验证阶段包含以下检验过程:
-
文件格式验证:验证字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理
- 是否以魔术0xCAFEBABE开头
- 主次版本号是否在当前虚拟机的处理范围之内
- 常量池中的常量是否有不被支持的类型。
-
元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;
- 这个类是否有父类。(除了java.lang.Object之外)
- 这个类的父类是否集继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
-
字节码验证:整个验证过程最复杂的一个阶段。主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似在操作栈放int型数据,使用却按long行加载如本地变量表中
- 保证跳转指令不会跳转到方法体意外的字节码指令上
-
符号引用验证:目的是确保解析动作能正常执行,发生在虚拟机将符号引用转换为直接引用的时候,这个转化动作将在连接的第三阶段-解析阶段中发生。
- 符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
符号引用中通过字符串描述的全限定名是否能够找到对应的类 - 在指定类中是否存在符号方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
- 符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
3. 准备
准备阶段给各个类变量(static修饰)分配内存,并设置类变量初始值,这些变量在方法区中分配。
需要注意的是“初始值”代表的不是我们在代码中敲的初始值,而是这个类型的相对“0”值。如:
public static int a = 100;
对于这句代码,在准备阶段的初始值为0,随后的初始化阶段才会被初始化为100。但当这个类变量还被final修饰时,准备阶段的初始值就变成了我们在代码里敲的值也就是100。附Java基本数据类型初始值:
除了基本数据类型外,类似String类的引用类型变量的初始值为null。
4. 解析
解析阶段虚拟机将常量池内的符号引用替换为直接引用,这些符号引用包括类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符。
5. 初始化
初始化才是执行我们敲的Java的开始阶段,按照我们的敲代码开始为非final修饰的类变量和其他变量复制,也就是说,初始化阶段是执行类构造器 < clinit>()方法过程。