原文链接:JVM系列:(六)JVM类加载步骤
一 什么是类加载
上一章我们了解了class文件存储结构,在class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。而虚拟机是如何加载这些class文件的?
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
JVM中类型的加载和连接过程都是在程序运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特性实现的。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。
类的完整生命周期包括:加载(loading)、验证(verification)、准备(preparation)、解析(resolution)、初始化(initialization)、使用(using)、卸载(unloading)。其中验证、准备、解析这三个步骤可以统称为连接(linking)。如下图所示:
![3690542-3aca2ffbcfe5ae46.png](https://upload-images.jianshu.io/upload_images/3690542-3aca2ffbcfe5ae46.png)
今天我们要说的类加载步骤不包括最后两步的使用和卸载,主要讲解步骤为:加载、验证、准备、解析和初始化。
二 类加载步骤
2.1 加载
加载阶段是虚拟机类加载的第一步骤,在加载阶段,虚拟机需要完成以下三件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在Java堆内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这些数据的访问入口。
虚拟机规范的这三点要求实际上并不具体,例如“通过一个类的全限定名来获取定义此类的二进制字节流”,并没有指明二进制字节流要从一个 class 文件中获取,没有指明要从哪里获取及怎样获取,例如:
- 从 ZIP 包中读取,这很常见,最终成为日后JAR、EAR、WAR 格式的基础。
- 从网络中获取,这种场景最典型的应用就是 Applet。
- 运行时计算生成,这种场景使用得最多的就是动态代理技术。
- 由其他文件生成,典型场景:JSP应用。
- 从数据库中读取,这种场景相对少见。
- ......
相对于类加载过程的其他阶段,加载阶段是开发期可控性最强的阶段,加载阶段既可以使用系统提供的类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以定义自己的类加载器去控制字节流的获取方式。
加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区中,然后在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口。
2.2 验证
这一阶段的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。我们前面已经说过,class文件不一定要求用Java源码编译而来,可以使用任何途径,包括用十六进制编辑器直接编写产生class文件。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。
大致上验证会完成下面四个阶段的检验过程:
- 文件格式验证:验证是否符合class文件格式的规范,并且能被当前版本的虚拟机处理。主要验证点就是我们上一章所说class文件结构的一些信息。这一阶段的验证是基于字节流进行的,经过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所以后面的三个验证阶段全部是基于方法区的存储结构进行的。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。例如是否有父类、父类是否被final修饰、类中的字段,方法是否与父类产生矛盾等。主要验证目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
- 字节码验证:主要进行数据流和控制流分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。例如保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作、保证跳转指令不会跳转到方法体外的字节码指令上、保证方法体中的类型转换是有效的等。
- 符号引用验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性的校验,通常需要校验的内容有符号引用中通过字符串描述的全限定名是否能找到对应的类、在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段、符号引用中的类、字段和方法的访问性(private、protected、public、default)是否可被当前类访问等。
2.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
这个阶段中需要注意的两点:
- 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不是实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆内存中。
- 这里所说的初始值“通常情况”下是数据类型的零值。例如int类型的初始化是0、long类型的初始值是0L等。真正给类变量赋定义值的动作将在下面的初始化阶段才会被执行。
2.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。包括:
- 类或接口的解析
- 字段解析
- 类方法解析
- 接口方法解析
2.5 初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(编译后的字节码)。
在准备阶段,变量已经被赋过一次变量默认的初始值,而在初始化阶段,则会根据程序中编写的具体值去初始化类变量和其他资源。
对于初始化阶段,虚拟机规范严格规定了有且只有四种情况必须立即对类进行“初始化”(加载、验证、准备自然需要在此之前开始);
- 遇到new、getstatic、putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条只能的最常见Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行发射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
有且仅有以上四种情况会执行类的初始化。
三 总结
类的加载步骤总结来说就是,先把远程或磁盘的class文件加载到内存中,并在Java堆内存生成该类的class对象以供外部访问、再对加载到的数据进行验证,校验是否符合JVM定义的class文件结构规范、其次再对定义的类变量赋初始值、最后就是对类变量进行赋定义值以及相关初始化工作。
接下来我们将介绍:
- 类加载器
![3690542-40a32f75e047e8c7.jpg](https://upload-images.jianshu.io/upload_images/3690542-40a32f75e047e8c7.jpg)