一、前言
这篇博客主要总结一下Java虚拟机中的类加载机制,刚开始学习Java时,相信大多数人第一步都是使用的java c的命令去编译.Java的文件,编译后产生了一个class文件,而类文件又是被JVM进行怎样的操作才能运行?读完这篇博客,相信你能有一个整体的了解。
二、加载过程概述
类从被加载到虚拟机的内存开始一直到被卸载出内存,生命周期一共包括了:加载、验证、准备、解析、初始化、使用、卸载7个阶段。如下图所示
其中验证、准备、解析统称为连接阶段,这里来一一介绍加载过程中每个阶段需要进行的操作。
加载
这个阶段,需要完成以下三件事:
1.通过一个类的全限定名(绝对路径)来获取定义此类的二进制流。
2.将这个字节流所代表的静态存储结构转化为方法区运行时的数据结构。
3.在内存中生成一个代表此类的java.lang.Class对象,作为方法区该类的各种数据的访问接口。
对Java内存区域不太了解的可以去看看我前面一篇博客Java内存区域。
这里由第一点,可以看出获取类的二进制流可以有多种不同的途径,正是如此,Java虚拟机给开发人员提高了很大的扩展性。这里注意,数组类不通过加载器创建,而是由Java虚拟机直接创建,但是数组的元素类型(去除维度后)最终还是由类加载器去加载。
加载阶段完成后,就开始连接阶段,注意加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证操作)是交叉进行的,接下来就介绍一下连接阶段。
验证
验证阶段是连接的第一步,这一步目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求。验证阶段要遵循的具体规范比较多,这里没必要写出来了,有兴趣的朋友可以去JVM的官网去看,这里重点是大体上了解整个流程。整体上看。验证阶段分为以下几个步骤:
1.文件格式验证
2.元数据验证
3.字节码验证
4.符号引用验证
准备
准备阶段是正式为类变量分配内存并设置变量初始值的阶段,这里注意一下,这个初始值不是开发人员给某个变量赋的值,而是不同的类型对应的“零值”。这些变量所使用的内存都将在方法区中进行分配,这时候进行内存分配的变量仅仅包括类变量(被static修饰的变量),实例变量将会在对象实例化时随着对象一起分配在Java堆中。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,这里还是先来简单的介绍一下符号引用于直接引用。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量(如我们常说的变量名),只要准确无误的能定位到目标就可以,与虚拟机的内存布局无关。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,与虚拟机的内存布局相关。
因为在程序中对同一个符号引用进行多次解析请求是常见的,所以虚拟机实现了对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并且把常量标识为已解析状态)。这里也是,具体的解析规范不多写,具体解析的步骤如下:
1.类或接口的解析
2.字段解析
3.类方法解析
4.接口方法解析
初始化
该阶段是类加载过程中的最后一步,前面的类加载过程中,除了开发人员可以通过自定义类加载器参与加载阶段外,其余动作都是都是完全由虚拟机主导和控制,到了这个阶段,才真正开始执行类中的程序代码。
之前在准备阶段,变量已经赋过一次初始值,而初始化阶段将根据开发人员定义的去进行赋值,换个角度看,初始化阶段是执行类构造器<clinit>()方法的过程。接下来就来详细的介绍一下该方法。
1.<clinit>()方法是由编译器自动收集类中的所有变量的赋值动作和static语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,并且金静态语句块只能访问到定义在静态语句块之前的变量。
2.<clinit>()方法与类的构造方法不同,不需要显示的调用父类构造器,但虚拟机会保证在子类的<clinit>()方法执行之前,父类的该方法已经执行完,所以第一个被执行的该方法肯定是Object类。
3.如果一个类中没有静态语句块,也没有变量的赋值操作,编译器可以不为这个类生成<clinit>()方法。这里注意,执行接口的<clinit>()方法不需要先执行父接口的该方法,并且接口的实现类在初始化时也不会执行接口的该方法。
4.虚拟机保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,并且通一个类加载器下,一个类型只会初始化一次。
初始化时,虚拟机还要遵守几条必须立即对类进行初始化的规范规范,也就是对类进行主动引用
1.使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外),以及调用一个类的静态方法的时候,类没有进行过初始化,则需要触发其初始化。
2.对类进行反射调用的时候。
3.初始化一个类时,其父类还没被初始化,则需要先触发其父类的初始化。
4.虚拟机启动时,优先初始化含main()方法的类。
类加载器
前面也多次提到了类加载器,这里来专门总结一下相关的知识,每一个类加载器都拥有一个独立的类名称空间,所以比较两个类是否相等,首先要比较的不是类名,而是是否来自同一个类加载器。说到类加载器。就离不开类加载器之间的关系模型。
双亲委派模型
类加载器从虚拟机的角度讲,一种是启动类加载器,一种是其它所有的类加载器。
先看看下图
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都需要有自身的父类加载器,并且类加载器之间的关系是以组合的关系来复用加载器代码。
工作过程
如果一个类的加载器收到了类加载的请求,首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,层层递进,因此所有的加载请求最终都被传送的顶层的启动类加载器中,只有当父类加载器无法完成请求时,子加载器才会自己去进行加载。
可以看出,双亲委派模型保证了Java程序的稳定运作,不会因为混乱的加载方式使程序无法正常加载运行。
最后说一下,现在很多的新技术都不是双亲委派模型中的树状结构,而是发展为更复杂的网状结构,这里还了解的不够多,就不必多说了。
总结
这篇博客系统的总结了一下JVM中的类加载机制,但更多的细节还是没有讲明,有兴趣了解更多的朋友的还是去读读《深入理解JVM虚拟机》,就暂时到这里了。