本文对java虚拟机的类加载机制进行了研究,该过程主要分为三个步骤:加载、连接、初始化,其中连接步骤可以分为:验证、准备、解析三个步骤。文中对于以上步骤进行了详细的介绍。此外,本文还涉及类加载器的部分知识:类加载器可以分为:启动类加载器、扩展类加载器、系统类加载器:java虚拟使用双亲委派的方式加载类。
关键词: 类加载、双亲委派模型、类加载器
This paper studies the class loading mechanism of java virtual machine. The process is mainly divided into three steps: loading, connecting and initializing. The connecting steps can be divided into three steps: verification, preparation and parsing. The article for a detailed description of the above steps. In addition, this article also covers part of the knowledge of the class loader: class loader can be divided into: start class loader, extended class loader, system class loader: java virtual use parents to load the class.
Keywords: class loading, parental delegate model, class loader
一、引言
虚拟机的类加载机制是虚拟把描述类的数据从class文件加载到内存中,并对数据进行校验、转化解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程。
在java中,类的加载、连接和初始化都是在程序运行期间完成的。这种策略在类加载时会增加一些开销,但是这也提供了更高的灵活性。Java天生可以动态扩展的语言特性就是引来运行期动态加载连接这个特点实现的。
二、类加载的时机
类从被加载到虚拟机内存开始到卸载出内存为止,整个生命周期包括七个部分:(1)加载;(2)验证;(3)准备;(4)解析;(5)初始化;(6)使用;(7)卸载,如图1.1所示。
图1.1java中类的生命周期
其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这样的顺序按部就班的进行,然而类的解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定。需要注意的是五个按部就班的阶段仅仅是按部就班的开始,然而他们的完成通常并不按照顺序,这是因为这些阶段通常来说是相互交叉混合进行的,一个阶段通常会调用另一个阶段。
对于类的加载,虚拟机规范没有严格的规定,虚拟机可以自行把控;对于类的初始化,虚拟机规范严格规定了有且仅有以下5种情况才进行类的初始化,以下情况都是类的主动调用:
(1)使用new、getstatic、putstatic、invokestatic指令时,需要初始化没有被初始化过的类。这些指令的使用场景是:new实例化对象时;调用类方法;读取设置类属性时。
(2)使用反射方法对类进行反射调用时。
(3)初始化一个类时,如果其父类还没有被初始化,那么要先初始化父类。
(4)虚拟机启动时,会加载main方法所在的类
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHan
dle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
下面,我们给出三个类被动调用,不进行初始化的例子。
(1)子类调用父类的静态属性时,只会初始化父类,而子类是否初始化不能得到保证。子类是否初始化取决于虚拟机的实现,虚拟机规范中没有明确规定。
(2)通过数组定义来引用类,不会触发类的初始化。
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
三、类加载的过程
3.1加载
在加载阶段,虚拟机需要完成三件事:(1)通过类的全限定名获取类的二进制字节流;(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
其中,虚拟机并没有指明二进制字节流从哪里获取,由此开发人员开发出了一些技术:(1)从ZIP包获取字节流(2)从网络获取(3)运行时计算生成(4)由其他文件生成(5)从数据库获取。
对于非数组类的加载阶段,类加载既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器完成。对于数组类的加载阶段,情况有些不同,数组类是由java虚拟机直接创建的。数组类中的元素由类加载器加载,数组类的创建遵循以下规则:
(1)如果数组的元素是引用类型,那么就递归采用本节中定义的加载过程去加载这个组件。
(2)如果数组的元素不是引用类型,那么虚拟机将会把数据标记为与引导类加载器关联。
数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。
类加载后,虚拟机将类的字节流按照虚拟机所需的格式存储在方法区中,虚拟机规范没有规定字节流在虚拟机方法区内的格式。加载阶段与连接阶段的部分内容交叉进行,但是这两个阶段的开始时间仍保持先后顺序。
3.2验证
验证是连接的第一步,这一步检查class文件的二进制字节流的信息是安全的,不会破坏虚拟机自身的安全。Java语言本身来说是一种相对安全的语言,它不允许数组越界,不允许跳转到不存在的代码行之类的事情,但是并不是所有的class文件都是由java文件编译来的,从理论上来说,虽然是java代码本身无法做到的事情,但是从语义上都是可以表达出来的。由此,虚拟机很有必要对二进制字节流进行验证,不然很有可能有恶意代码破坏虚拟机的安全,这个阶段也是十分重要的。其中,验证主要有以下四部分验证动作:
(1)文件格式验证。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。
(2)元数据验证。第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点有:该类是否有父类;该类是否继承了不被允许的继承的类;如果该类非抽象,那么该类是否实现父类或接口中所有要求实现的方法。
(3)字节码验证。该验证部分的木笔是通过数据流和控制流的分析,确定程序的语义是合法的符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
(4)符号引用验证。该校验在虚拟机将符号转化为直接引用时发生,这个转化动作将在连接的第三阶段,解析阶段发生。需要校验的内容有:符号引用中通过字符串描述的全限定名是否能够找到对应的类等。
3.3准备
准备阶段为类变量在方法区内分配内存并设置初始值。这里需要明确的是,分配内存的仅是被static修饰的变量,不包括实例变量,实例变量是在实例化一个对象时,与对象一起被创建在堆里的。这里需要注意的另外一点是,如有类变量在定义时被显示的初始化了,那么该类变量在准备阶段的值为显示设置的值。
3.4解析
解析阶段将虚拟机常量池中的符号引用转换为直接引用的过程。我们首先明确一下什么事符号引用,什么事直接引用。
符号引用:它以一组符号来描述所引用的目标,符号可以是任何形式的字面量。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。
对于同一个符号,可能存在虚拟机解析多次的情况,这时虚拟机可能会将该符号的解析结果保存在缓存中从而避免解析动作重复进行。解析过程主要针对于类、接口、字段、类方法、方法类型、接口方法、电影那个点限定符、方法句柄进行。
3.5初始化
类初始化阶段是累加载过程的最后一个部分,在前面部分的类加载过程中,基本上都是有虚拟机主导和控制的,知道初始化阶段才真正开始执行类中定义的java代码。可以说,初始化阶段是执行类构造器方法的过程。类构造器是由编辑器自动收集类中所有变量的赋值动作和静态语句块中的语句产生的。虚拟机会保证在子类的类构造方法执行之前,父类的类构造方法已经执行完成。虚拟机会保证一个类的类构造方法在多线程环境中被正确的同步、加锁,不存在线程不安全的情况。
四、类加载器
4.1类与类加载器
实现通过类的全限定名来获取描述此类的二进制字节流的动作的代码块称为类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。对于两个类是否相等,只有在这两个类都是由同一个类加载器加载时才有意义。只要加载类的加载器不同,那么这两个类就一定不同。
4.2双亲委派模型
对于java语言来说大致可以分为两种类加载器:(1)启动类加载器:该加载器使用C++编写,是虚拟机的一部分;(2)其他类加载器,该加载器由java编写,独立于虚拟机,均为classLoader的子类。
开发人员对于加载器细致的分为三种:(1)启动类加载器,这个类将器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的类库加载到虚拟机内存中。该加载器无法直接被java程序引用;(2)扩展类加载器:该加载器sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;(3)应用程序类加载器,它由sun.misc.Launcher$AppClassLoader实现。这个类加载器一般也称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。双亲委派模型使得Java类随着它的类加载器一起具备了一种带有优先级的层次关系。如图3.1所示。
图3.1 双亲委派模型
参考文献
[1]张华伟,魏庆.Java运行原理与Java虚拟机[J].光盘技术,2009(10):40-42.
[2]闫伟,谷建华.Java虚拟机即时编译器的一种实现原理[J].微处理机,2007(05):58-60.
[3]黄明,刘阳.Java虚拟机加载机制浅析[J].科技咨询导报,2007(27):12+14.
[4]郑艳玲. JAVA虚拟机相关技术研究与实践[D].西南交通大学,2007.
[5]王立冬,张凯.Java虚拟机分析[J].北京理工大学学报,2002(01):60-63.