Java虚拟机将描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称为虚拟机的类加载机制。
与那些在编译时需要进行连接的语言不同,Java语言中,类的加载、连接和初始化过程都是在程序运行期间完成,这种策略让Java语言进行提前编译会面临额外的困难,也会让类加载时稍微增加一些性能开销,但是却为Java应用提供了极高的扩展性和灵活性,Java语言天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态链接这个特效实现。
在开始正式介绍类加载机制前,先做以下约定:
一个类的生命周期将会经历**加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unlaoding)**七个阶段。具体的顺序如图:
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,而解析阶段在特定情况下可以在初始化阶段之后在开始,这是为了支持Java的运行时绑定特点。另外,这里说的确定的顺序,是指按确定的顺序开始,但不是按确定的顺序完成,这些阶段通常都会互相交叉地混合进行。
对于什么时候开始类加载的第一阶段“加载”,《Java虚拟机规范》并没有进行强制约束。但是对于初始化阶段,《Java虚拟机规范》严格规定有且仅有六种情况必须立刻对类进行初始化(在此之前,自然需要完成加载、验证、准备):
这六种场景中的行为称为对一个类型的主动引用,除此之外,所以引用类型的方式都不会出发初始化,称为被动引用。
接口的初始化时机与类有些微区别,主要是对应前述的6种场景中的第三种,对于接口,在初始化时并不要求其父接口全部完成初始化,而是只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化父接口。
类的加载过程分为:加载、验证、准备、解析和初始化这五个阶段。
JVM虚拟机在加载阶段主要完成以下三件事:
这里有几个需要注意的点
a. 二进制字节流的获取
《Java虚拟机规范》中并没有指明二进制字节流的获取必须从某个Class文件中获取,JVM允许各种方式,包括:从ZIP压缩包中读取(JAR、WAR)、从网络中获取(Web Applet)、运行时计算生成(动态代理)、从其他文件生成等等…
b. 非数组类型的加载
非数组类型的加载(严格来说是加载阶段获取二进制字节流的动作)是开发人员可控性最强的阶段,可以通过JVM内置的启动类加载器完成,也可以由用户自定义的类加载器去完成。
c. 数组类型的加载
数组类本身不通过类加载器创建,它是由JVM直接在内存中动态构造出来的。但是,数组类的元素类型(Element Type,数组去除所有维度的类型)最终还是要靠类加载器来完成加载。一个数组类的创建需要遵循以下规则:
验证阶段的目的是为了确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
验证阶段大致上分为以下四个阶段:
a. 文件格式验证
主要目的是保证输入的字节流能正确地解析并存储于方法区。
这阶段的验证是基于二进制字节流进行的,验证通过后字节流才会被允许进入JVM中的方法区中,后续的三个验证阶段都是基于方法区的存储结构进行的,不再直接读取、操作字节流。
b. 元数据验证
主要目的是对类的元数据信息进行语义校验,保证不存在与《Java虚拟机规范》定义相悖的元数据信息
c. 字节码校验
主要目的是通过数据流分析和控制流分析,确定程序语义是合法的。这阶段要对类的方法体信息校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
但是,即使通过字节码校验,也不能保证就一定是安全的,还是无法避免"停机问题"。通过程序去校验程序逻辑是无法做到绝对准确的,不可能用程序来判断一段程序是否存在Bug。
为了简化字节码校验阶段的耗时,JVM虚拟机设计团队将尽可能多的校验辅助措施放到了Javac编译器中进行,具体做法是给方法体Code的属性表中新增一项名为"StackMapTable"的新属性。借助该属性,JVM只需要检查该属性的记录是否合法即可,这样就将字节码验证的类型推导转变为类型检查,节省了大量校验时间。
d. 符号引用校验
该阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段发生。
该阶段的目的是保证解析行为能正常执行,如果不通过JVM将抛出java.lang.ImcompatibleClassChangeException的子异常。
最后,验证阶段虽然很重要,但是却不是必须要执行的。如果程序运行的全部代码都已经被反复使用和验证过,在生产环境的实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,缩短类加载时间。
该阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
关于该阶段需要注意以下两点:
解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
直接引用:可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄
解析主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定负这7类符号引用进行。
除了在加载阶段应用程序可以通过自定义类加载器的方法局部参与之外,其余的阶段都完全由JVM来主导控制。到了初始阶段,JVM才开始真正执行类中编写的Java程序代码,将主导权移交给应用程序。
初始化阶段就是执行类构造器
方法的过程。
有关
方法有以下细节:
()
方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中(static{}块)的语句合并产生的()
方法和类构造器不同,不需要显示地调用父类构造器,JVM会保证在子类的()
方法执行前,父类的()
方法已经执行完毕。JVM中第一个被执行的()
方法的类型肯定是java.lang.Object()
方法先执行,因此父类中定义的静态语句块要优先于子类的变量赋值操作()
方法对于类或者接口来说并不是必须的,如果一个类中没有静态语句块,也没有对变量的赋值操作,编译器可以不为这个类生成()
方法()
方法不需要先执行父接口的()
方法,因为只有当父接口中定义的变量被使用时,父接口才会被初始化。同时,接口的实现类在初始化是也不会执行接口的()
方法()
方法在多线程环境中被正确地加锁同步JVM设计团队将类加载阶段中的"通过一个类的全限定类名来获取描述该类的二进制字节流"这个动作放到了JVM外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被成为"类加载"。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确定其在JVM中的唯一性。
比较两个类是否"相等",只有在这两个类是由同一个类加载加载的前提下才有意义。否则,即使两个类来源于同一个Class文件,被同一个JVM加载,只要加载它们的类加载器不同,这两个类就一定不相等。
站在JVM的角度看,只有两种不同的类加载器:一种是启动类加载器,由C++语言实现;一种是其他所有的类加载器,由Java语言实现。
站在开发者的角度看,有三种不同的类加载器:启动类加载器、扩展类加载器和应用程序类加载器
启动类加载器(Bootstrap Class Loader)
负责加载存放在
扩展类加载器(Extension Class Loader)
负责加载
启动类加载器(Application Class Loader)
负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器
自定义类加载器(User Class Loader)
用户可以自定义类加载器,只需要继承ClassLoader类并重写findClass(String className)方法即可。
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器,其余的类加载器都应该有自己的父类加载器。注意,这里的父子级关系并不是以继承实现,而是通过组合关系来复用父加载器的代码。
工作过程:
如果一个类加载起收到了类加载的请求,首先会将这个请求委托给父类加载器去完成,每一层的类加载器都是如此,最终请求会被委托到启动类加载器。只有父类加载器反馈自己无法完成这个请求时,子加载器才会尝试自己去完成加载。
好处:
双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们使用的类加载器实现方式。知道Java模块化出现为止,该模型一共经历了三次大规模的"被破坏"的情况
第一次破坏
双亲委派模型出现在JDK 1.2之后,而在这之前已经有类加载器的概念和抽象类java.lang.ClassLoader。
在JDK 1.2之后,面对已经存在的用户自定义类加载器的代码,Java设计者们引入双亲委派模型是不得不做一些妥协,在java.lang.ClassLoader中添加一个新的protected方法findClass(),并引导用户编写的类加载起尽可能去重写这个方法,而不是在loadClass中编写代码。
第二次破坏
双亲委派模型保证了越基础的类由越上层的加载器进行加载,但是如果有基础类型又要调用回用户的代码,怎么处理呢?
Java设计团队为此引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载起可以通过java.lang.Thread类的setContextClassLoader()方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,这个类加载器默认就是应用程序类加载器。
第三次破坏
这次破坏是由于用户对程序动态性的追求所导致的,所谓的动态性包括:代码热替换、模块热部署等,用户希望Java应用程序能像电脑外设一样,街上鼠标、U盘,不需要重启就能立刻使用。
在Java的模块化规范战役中,IBM提出了OSGi项目。OSGi中,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle的时候,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型推荐的树状结构,而是更为复杂的网状结构。