Java虚拟机类加载机制

Java程序运行于Java虚拟机之上,JVM屏蔽了底层细节,使得Java程序可以“一次编译,处处运行”。在Java语言中,一切皆是对象,代码通常由类、接口、enum等构成,是一种面向对象的编程语言。本文将为你揭示Java虚拟机如何加载类,一窥Java底层的秘密。java

类在虚拟机中的生命周期,能够分为加载、验证、准备、解析、初始化、使用、卸载几个阶段,其中的验证、准备、解析统称为链接。在这里,读者能够回忆一下以C语言为表明的面向过程语言如何实现动态连接库,以更好地理解Java面向对象编程。c++

类的生命周期

一般状况下,虚拟机都会按照上图流程管理类的生命周期。然而,Java语言的一大特性——多态支持方法的动态绑定,即,调用方法前没法知道具体调用了那个方法,只有运行到调用的时刻才能肯定方法的具体实现。所以,解析也可能发生在初始化以后,在多态调用时才解析出具体的直接引用。spring

 

加载

在Java虚拟机规范中,并无强制要求何时加载类,由虚拟机自行把握。在加载阶段,虚拟机经过一个类的全限定名获取类的二级制字节流,把字节流的静态存储结构转换为运行时数据结构,在内存中生成一个Class对象,Class对象将做为方法区的访问入口。编程

在Java中,可以根据全限定名获取字节流的代码块被称为类加载器。主要包括启动类加载器、扩展类加载器、应用程序类加载器和用户自定义类加载器。其中,安全

启动类加载器加载jre的lib目录下的类,如rt.jar,在Hotspot虚拟机中用c++实现,是虚拟机的一部分;网络

扩展类加载器加载jre的lib/ext或者由系统变量 java.ext.dir指定目录中的类,通常Java语言实现;数据结构

应用程序类加载器加载CLASSPATH中的类,通常Java语言实现;多线程

自定义类加载器用于程序实现个性化的类加载,如spring提供的ClassLoader、用于热升级的ClassLoader、从网络加载jar包的ClassLoader。并发

在加载类的过程当中,Java采用了双亲委派机制。而这种父子关系并非经过继承实现的,而是组合关系。一个类加载器须要加载类时,首先委托父类加载器进行加载,并逐级向上,若是父类加载器加载成功则返回成功,若是父类加载器加载失败,则本身进行加载。在Java中,类的惟一性是由类和所属的类加载器共同肯定的。两个类加载器加载的同一个class,在虚拟机看来也是不一样的类。经过双亲委派机制,Java可以保证核心类不会被用户覆盖,因用户企图覆盖核心类时类加载器总能找到已由父类加载器加载的核心类。编程语言

Java双亲委派机制

 

验证

只要符合class文件的格式要求的class文件都能被虚拟机加载,无论class文件是否是由Java编译器所产生。Java虚拟机出于自身安全的考虑,会对加载的类进行合法性验证。

在验证阶段,虚拟机将进行文件格式验证、元数据验证、字节码验证和符号引用验证。此阶段主要的目标是确保Class文件的字节流包含的信息符合虚拟机的要求。

文件格式验证:验证字节流是否符合Class文件格式的规范,而且能被当前版本的虚拟机处理。如是否以魔数0xCAFEBABE开头,主、次版本号是否在当前虚拟机处理范围以内等。

元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,如是否有父类,父类是否继承了不容许被继承的类,类中的字段、方法是否与父类产生矛盾等。

字节码验证:对类的方法体进行校验分析,保证被校验类的方法在运行时不会作出危害虚拟机安全的事件。如保证跳转指令不会跳转到方法体之外的字节码指令上。

符号引用验证:对类自身之外(常量池中的各类符号引用)的信息进行匹配性校验,如符号引用中经过字符串描述的全限定名是否能找到对应的类,符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问等。

 

准备

在准备阶段,虚拟机将为类变量在方法区分配内存并设置类变量的初始值。此时的初始值并非源代码中的初始值,而是各类类型变量的默认初始值,如int类型为0、boolean类型为false。源代码中的变量初始值,会在<clinit>方法中赋值,在初始化阶段完成。

 

解析

在解析阶段,虚拟机将常量池内的符号引用替换为直接引用。在类初始化以前,解析操做只能解析静态绑定的符号引用。

符号引用以一组符号来描述所引用的目标,符号能够是任何形式的字面量,只要使用时能无歧义地定位到目标便可。符号引用与虚拟机实现的内存布局无关,引用的目标并不必定已经加载到内存中。

直接引用能够是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不一样虚拟机实例上翻译出来的直接引用通常不会相同。若是有了直接引用,那引用的目标一定已经在内存中存在。

 

初始化

虚拟机规范要求有且只有如下几种状况触发类的初始化,也就是说链接以后的类并无当即执行初始化,而是在使用前才进行初始化:

①使用new建立对象、读写类的静态字段、调用类的静态方法时须要进行初始化,但final修饰的字段除外。读写静态字段时,只有静态字段所在的类会被初始化。

②使用反射调用的时候,若是类没有初始化则先初始化。

③初始化一个类的时候,若是它的父类尚未初始化,则先触发其父类的初始化。

④虚拟机启动时,用于执行的包含main方法的类须要先初始化;

⑤使用动态语言支持时,若是解析结果引用的类没有进行初始化,则须要先初始化。

在编译阶段,编译器会扫描源文件,根据类中的变量赋值和静态语句块生成<clinit>方法,并在初始化阶段执行<clinit>方法。

<clinit>方法中初始化过程与源代码中语句顺序保持一致,静态语句块只能访问以前的变量,对于以后的变量只能赋值不能访问。若是类中没有变量赋值和静态语句块,则不会生成<clinit>方法。在讲解继承的时候,一般都会提到父类会先于子类进行初始化,必定程度上也是由于父类的<clinit>方法会先于子类执行。

若是接口定义了常量,也会生成<clinit>方法,与类不一样的是,接口初始化时不须要先调用父接口的<clinit>方法,只有在用到父接口的变量时才执行父接口的<clinit>方法。而且,接口的实现类在初始化时也不会调用接口的<clinit>方法,所以方法属于接口不属于实现类。

在初始化过程当中,虚拟机会保证多线程并发状况下类可以被正确初始化,即<clinit>方法会被虚拟机加锁和同步,同一时间只有一个线程可以执行<clinit>方法。

 

总结

虚拟机的类加载机制,分为加载、验证、准备、解析、初始化五个阶段。采用双亲委派机制从类的文件二进制流载入,而后进行类的合法性验证,分配类的运行时数据空间,对符号引用进行解析转为直接引用,最后执行类的初始化。

 

原文地址:Java虚拟机类加载机制

相关文章
相关标签/搜索