深入理解Java虚拟机 3.JVM类加载机制详解
我们知道,Java源文件是不能直接运行的,首先需要编译成字节码(.class)文件,然后JVM在运行时,会把字节码文件加载到虚拟机内存中,对数据进行校验、转换解析和初始化之后,最终形成被可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。本文将对虚拟机中除了垃圾回收机制之外的另一个重要的机制,那就是类加载机制进行详细解析。
在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的。这样虽然会使得类加载是稍微增加一些性能开销,但是会为java应用程序提供高度的灵活性,像Java里面的多态性、动态代理等都是依赖于运行期间动态加载和动态链接的特性实现的。
JVM虚拟机的类加载过程,主要分为5个过程:加载、验证、准备、解析、初始化。其中验证、准备、解析三个部分统称为连接。而类从被加载到虚拟机内存开始,到卸载存内存为止,这些过程可交叉运行。整个生命周期如下:
1.类加载过程解析
1.1 加载
类加载的时机在虚拟机规范中并没有竞争性强制约束,根据虚拟机的不同实现而有所不同。但是在出现以下五种情况时,有且只有这五种情况时,虚拟机需立即对类进行初始化,当然也就必须立即对类执行加载、验证、准备过程。五种情况如下:
1.遇到new、getstatic、putstatic或invokesattic这四条字节码指令时,如果累没有进行过初始化,则需立即触发其初始化。生成这四条字节码指令的操作如以下场景:使用new关键字实例化对象、读取/设置一个类的静态字段、调用一个类的静态方法。
2.使用java.lang.reflect包的方法对类进行反射调用的时候,如果累没有进行过初始化,则需立即触发其初始化。
3.当初始化一个类时,发现其父类还没有进行过初始化,则需立即触发其父类初始化。
4.当虚拟机启动时,需执行一个包含程序入口main方法的类,该类需要立即进行初始化。
5.当使用JDK1.7动态语言支持时,如果一个java.lang.invoke.methodhandler的实例最后解析结果为REF_getstatic、REF_putStatic、REF_invokeStatic的方法句柄时,并且该方法句柄对应的类没有进行过初始化,则需要立即触发其初始化、
加载阶段主要完成以下三件事情:
1.通过一个类的全限定类名来获取定义次类的二进制字节流。
2.将这个类所代表的的静态存储结构转化为方法区的运行时数据结构。
3.在内存中生成一个代表该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
1.2 验证
验证阶段的主要目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,能被当前版本的虚拟机处理,并且不会危害到虚拟机自身的安全。验证阶段的工作量战狼JVM类加载中相当大的一部分,因为验证阶段的严谨与否,直接决定了虚拟机是否能承受恶意代码的攻击。
主要包括文件格式的验证、元数据的验证、字节码验证、符号引用验证。
1.3 准备
准备阶段是正式为类变量分配内存并设置类变量的初始值的阶段,为其在方法区中分配内存。需要注意的是准备阶段的内存分配只是类变量(被static修饰的变量),而不包括实例变量,实例变量是在初始化阶段对象实例化的时候分配内存。而且准备阶段的赋值只是为类变量赋默认值,而不是具体定义的值。如下:
public staticint value = 999;
在准备阶段,value的值是0而不是999。而将value赋值为999的过程发生在初始化阶段,通过putStatic指令实现。
1.4 解析
解析阶段的主要作用是将常量池内的符号引用替换成直接引用。所谓符号引用就是指一组用来描述所引用目标的符号,直接引用是指直接指向目标的指针或者一个能间接定位到目标的句柄等。
1.5 初始化
类初始化是类加载过程中的最后一步,真正开始执行类中定义的Java程序代码(或者说是字节码)。执行变量的真正初始化工作。
2. 类加载器详解
类加载器用于实现类的加载动作,对于任意一个类,都需要有加载他的类加载器和这个类本身一同确立起在Java虚拟机中的唯一性。比较两个类是否相同,只有在这两个类是由同一个类加载器加载的前提下才有意义。
2.1 类加载器介绍
从JVM的角度看,类加载器分为两类:启动类加载器、其他类加载器。启动类加载是JVM自身的一部分,使用C++语言实现。其他所有的类加载器,都是由Java语言实现,独立于虚拟机之外,并且对于HotSpot虚拟机来说,其他类加载器全部继承自java.lang,ClassLoader类。
从Java开发人员的角度看,主要有以下三种类型的类加载器:启动类加载器、扩展类加载器、扩招程序类加载器。启动类加载器负责将存放在\lib目录下的文件加载到虚拟机中。扩展类加载器负责将\lib\ext目录中的文件加载到虚拟机中。而应用程序类加载器,负责加载用户类路径下的文件。如果应用程序中没有自定义过类加载器,一般情况这个类加载器就是程序中默认的类加载器。
2.2 双亲委派模型
双亲委派模型:双亲委派模型是指除了顶层的启动类加载器外,其余类加载器都用该有自己的父类加载器。这里的父子关系是通过组合关系而不是继承关系是实现复用父类加载器的代码。
双亲委派模型的工作过程:如果一个类加载器收到类加载的请求,它首先会将这个请求委派给父类加载器去完成(所有的加载请求都应该最终传送到顶层的启动类加载器中),只有父类加载器返回其无法完成加载请求的时候,子类加载器才会尝试自己去加载。
双亲委派模型的破坏:双亲配拍模型的破坏主要有三次较大的情况出现。
1.第一次破坏是因为类加载器和java.lang.ClassLoader在JDK1.0时候就已经存在,,而双亲委派模型是JDK1.2的时候才加入的。在1.0版本的时候,用户可以通过继承classLoader类,然后重写父类的loadClass方法来自定义类加载器,这种情况明显不符合双亲委派模式的规则。所以在1.2版本后,不提供用户通过重写classLoader方法去实现自动以类加载器。而是新增加了一个findClass方法,在findClass方法中实现自定义类加载过程。如果父类的classLoader方法加载类失败,则在子类的classLoader方法中调用findCLass方法来完成类加载请求。
2.第二次破坏是因为模型自身的设计缺陷导致。存在这样一种情况,基类的类加载器需要调用用户的代码,但是基类的类加载器并不认识用户的代码。为此Java设计团队引入“线程上下文类加载器”,这样通过基类加载器去请求子类加载器来完成类加载的动作,这样也就违背了双亲委派模型的规则。
3.第三次破坏是由于用户对程序动态性的追求导致,像代码热替换、模块热部署等。在Java中是通过OGSI自定义的类加载器机制实现的。而OGSI中的类加载器是不符合双亲委派模型的。