虚拟机类加载机制

来源:互联网 发布:vmware14运行ubuntu 编辑:程序博客网 时间:2024/06/08 04:37

概述

虚拟机的类加载机制:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

在Java语言中,类型的加载、连接和初始化过程都是在程序运行期间完成的:增大了类加载时的开销,但提供了高度的灵活性(可以动态扩展的语言特性):面向接口的应用程序,可以等到运行时再指定其实际的类;可以通过Java预定义和自定义类加载器,让一个本地程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分。

类加载的时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括了:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称链接。

虚拟机类加载机制

加载(装载)、验证、准备、初始化和卸载这五个阶段顺序是固定的,类的加载过程必须按照这种顺序开始,而解析阶段不一定;它在某些情况下可以在初始化之后再开始,这是为了运行时动态绑定特性。值得注意的是:这些阶段通常都是互相交叉的混合式进行的,通常会在一个阶段执行的过程中调用或激活另外一个阶段。

有且只有以下四种情况必须立即对类进行”初始化”(称为对一个类进行主动引用):

  1. 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时(使用new实例化对象的时候、读取或设置一个类的静态字段、调用一个类的静态方法)。
  2. 使用java.lang.reflet包的方法对类进行反射调用的时候。
  3. 当初始化一个类的时候,如果发现其负类没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,虚拟机会初始化主类(包含main方法的那个类)。

被动引用:

  1. 通过子类引用父类的静态字段,不会导致子类初始化(对于静态字段,只有直接定义这个字段的类才会被初始化)。
  2. 通过数组定义类应用类:ClassA [] array=new ClassA[10]。并没有对ClassA进行初始化,但却触发了一个名为[LClassA的类的初始化,它是一个由虚拟机自动生成的、直接继承于Object的类,创建动作由字节码指令newarray触发。
  3. 常量会在编译阶段存入调用类的常量池。

实例初始化方法(名为<init>)和类与接口初始化方法(名为<clinit>)。

编译器会为接口生成<clinit>()构造器,用于初始化接口中定义的成员变量。一个接口在初始化时,并不要求其父类接口全部完成了初始化,只有在真正使用到父接口的时候才会初始化。

类加载的过程

加载

加载阶段是“类加载机制”中的一个阶段,这个阶段通常也被称作“装载”,主要完成:

  1. 通过“类全名”来获取定义此类的二进制字节流
  2. 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

虚拟机规范对于“通过“类全名”来获取定义此类的二进制字节流”并没有指明二进制流必须要从一个本地class文件中获取,准确地说是根本没有指明要从哪里获取及怎样获取。例如:

  1. 从Zip包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  2. 从网络获取,常见应用Applet。
  3. 运行时计算生成,这种场景使用的最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用ProxyGenerator.generateProxyClass来为特定接口生成$Prxoy的代理类的二进制字节流。
  4. 由其他格式文件生成,典型场景:JSP应用
  5. 从数据库中读取,这种场景相对少见,有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。

相对于类加载过程的其他阶段,加载阶段(准备地说,是加载阶段中获取类的二进制字节流的动作)是开发期可控性最强的阶段,因为加载阶段可以使用系统提供的类加载器(ClassLoader)来完成,也可以由用户自定义的类加载器完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式有虚拟机实现自行定义,虚拟机并未规定此区域的具体数据结构。然后在java堆中实例化一个java.lang.Class类的对象,这个对象作为程序访问方法区中的这些类型数据的外部接口。

对于数组类而言,情况就有所不同,数组泪本身不通过类加载器创建,它是由虚拟机直接创建的,但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建,一个数组类创建过程需要遵循以下规则:

  1. 如果数组的组建类型(指的是数组去掉一个维度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组建的类加载器的类名称空间上被标识。
  2. 如果数组的组建类型不是引用类型(例如int[] arr),Java虚拟机将会把数组C标记为与引导加载器一起确定唯一性。
  3. 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

加载阶段与链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于链接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证时链接阶段的第一部,这一阶段的目地市确保Class文件的字节流中包含的信息符号当前虚拟机的要求,并且不会危害虚拟机自身安全。
验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度来说,验证阶段的工作量在虚拟机的类加载子系统中又占类相当大的一部分,从整体来说,验证阶段主要包括以下四个阶段。

文件格式的验证

  1. 是否以魔术0xCAFEBABY开头
  2. 主次版本是否在当前虚拟机处理范围之内
  3. 常量池的常量是否有不被支持的常量类型
  4. 指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
  5. Class文件中各个部分及文件本身是否被删除的或附加的其他信息

这个阶段的验证时基于二进制字节流进行的,只有通过了这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所以后面三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流

元数据验证

第二阶段是对字节码描述信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括以下验证:

  1. 这个类是否有父类
  2. 这个类的父类是否继承类不被允许继承的类(被final修饰的类)
  3. 如果这个类不是抽象类,是否实现类其父类或接口之中要求实现的所有方法
  4. 类中的字段、方法是否与父类产生矛盾

这个阶段的主要目的是对类的元数据进行语义检查,保证不存在不符合Java语言规范的元数据信息。

字节码验证

第三阶段是整个验证中最复杂的一个阶段,主要目地市通过数据流和控制流程分析,确定程序语义是合法的、符合逻辑的。在第二个阶段对元数据信息中的数据类型昨晚校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

  1. 保证任意时刻操作数栈的数据类型与质量代码序列都能配合工作,例如不会出现类似这样的情况:在操作数栈放类一个int类型数据,使用时却按long类型来加入本地变量表
    保证跳转指令不会跳转到方法体以外的字节码指令上
  2. 保证方法体重的类型转换时有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,这是危险并且不合法的/。

如果一个类方法体字节码没有通过字节码验证,那肯定是有问题的:但如果一个方法体通过类字节码验证不一定就是安全的:停机问题。

为了避免浪费时间在数据流验证的高复杂性上,JDK 1.6在方法体的Code属性的属性表中增加了一项名为“StackmapTable”的属性,这项属性描述了方法体中所有的基本块(按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节码验证期间,就不需要根据程序推导这些状态的合法性。这样将字节码验证的类型推导转变为类型检查从而节省一些时间。

符号引用验证

最后一个验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三个阶段—-解析阶段中发生。符号引用验证可以看作是对类自身以外的信息进行匹配性校验。

  1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
  2. 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  3. 符号引用中的类、字段、方法的访问性是否可以被当前类访问

    符号引用验证的目地市确保解析动作能正常执行,如果无法通过符号引用验证。那么将会抛出如下异常:java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等
    对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要的阶段。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配

这个阶段中有两个容易产生混淆的知识点,首先是这时候进行内存分配的仅包括类变量(static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量定义为:
public static int value = 12;
那么变量value在准备阶段过后的初始值为0而不是12,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为12的动作将在初始化阶段才会被执行。
上面所说的“通常情况”下初始值是零值,那相对于一些特殊的情况,如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,建设上面类变量value定义为:
public static final int value = 123;
编译时javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value设置为123。

解析

解析阶段是在虚拟机将常量池内的符号引用替换为直接引用的过程。

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

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或者一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

类或接口(对应于常量池的CONSTANT_Class_info类型)的解析:

假设当前代码所处的类为D,需要将一个从未解析过的符号引用N解析为一个类或接口C的直接引用:

  1. 如果C不是一个数组类型,虚拟机将会把代表C的全限定名传递给D的类加载器去加载这个类。
  2. 如果C是一个数组类型,并且数组的元素类型为对象(N的描述符类似
    [Ljava.lang.Integer),将会加载数组元素类型(java.lang.Integer),接着由虚拟机生成一个代表此数组维度和元素的数组对象。
  3. 如果以上过程没有发生异常,则C在虚拟机中已经成为了一个有效的类和接口了,之后还要进行的是符号引用验证,确认D是否具有对C的访问权限,如果没有,将抛出java.lang.IllegalAccessError异常。

字段(对应于常量池的CONSTANT_Fieldref_info类型)解析:

对字段表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个字段所属的类或接口。

  1. 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。
  2. 否则,如果C实现了接口,则会按照继承关系从下往上递归搜索各个接口和他的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。
  3. 否则,如果C不是java.lang.Object类型的话,将会按照继承关系从下往上递归的搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用。
  4. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。

虚拟机的编译器实现可能会更严格:如果一个同名字段同时出现在C实现的接口和父类中,或者同时在自己或父类的多个接口中出现,编译器将可能拒绝编译。

类方法(对应于常量池的CONSTANT_Methodref_info类型)解析:

对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。

  1. 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现class_index中索引的C是个接口,则抛出java.lang.IncompatibleClassChangeError。
  2. 在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
  3. 否则,在C的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
  4. 否则,在C**实现的接口列表及它们的父接口中递归**的查找是否有简单名称和描述符都与目标相匹配的方法,如果有说明C是个抽象类,查找结束,抛出java.lang.AbstractMethodError异常。
  5. 否则,查找失败,抛出java.lang.NoSuchMethodError异常。

如果查找返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对这个方法的访问权限,则抛出java.lang.IllegalAccessError异常。

接口方法(对应于常量池的CONSTANT_InterfaceMethodref_info类型):

对方法表中的class_index项中索引的CONSTANT_Class_info符号引用进行解析。用C表示这个方法所属的类或接口。

  1. 如果在接口方法表中发现class_index中索引的C是个类,则抛出java.lang.IncompatibleClassChangeError。
  2. 否则,在接口C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
  3. 否则,在接口C的父接口中递归查找,知道java.lang.Object类(包括在内),看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用。
  4. 否则,查找失败,抛出java.lang.NoSuchMethodError。

初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。
在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主管计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器()方法的过程.
<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
<clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类()方法执行之前,父类的()方法方法已经执行完毕
由于父类的()方法先执行,也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作。
<clinit>()方法对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生产()方法。
接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法但接口与类不同的是,执行接口的()方法不需要先执行父接口的()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的()方法。

虚拟机会保证一个类的()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有好事很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。

类加载器

类加载器虽然只用于实现类的加载动作,但是它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在JVM中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则即使这两个类来源于同一个文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就一定不相同。
双亲委派模型
从Java虚拟机的角度来说,只存在两种不同的类加载器:
一种是启动类加载器,是虚拟机自身的一部分。 这个类加载器使用C++语言实现,并非ClassLoader的子类。主要负责加载存放在JAVA_HOME / jre / lib / rt.jar里面所有的class文件,或者被-Xbootclasspath参数所指定路径中以rt.jar命名的文件。
另一种是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自ClassLoader类。

  1. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载AVA_HOME / libext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  2. 应用程序类加载器(Application ClassLoader):这个加载器由sun.misc.Launcher$AppClassLoader实现,它负责加载classpath对应的jar及目录。一般情况下这个就是程序中默认的类加载器。
  3. 自定义类加载器(User Defined ClassLoader):开发人员继承ClassLoader抽象类自行实现的类加载器,基于自行开发的ClassLoader可用于并非加载classpath中(例如从网络上下载的jar或二进制字节码)、还可以在加载class文件之前做些小动作如:加密等。

类加载器分类
双亲委派模型是这样的:如果一个类加载器收到了类加载的请求,它首先不会对自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去加载。

使用双亲委托模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object存放在rt.jar之中,无论那个类加载器要加载这个类,最终都是委托给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类,相反,如果没有双亲委托模型,由各个类加载器去完成的话,如果用户自己写一个名为java.lang.Object的类,并放在classpath中,应用程序中可能会出现多个不同的Object类,java类型体系中最基本安全行为也就无法保证。

类加载的几种方式

  1. 命令行启动应用时候由JVM初始化加载,加载含有main的主类。
  2. 通过Class.forName()方法动态加载类,默认会执行初始化块。如果指定ClassLoader,则不会执行初始化块。
  3. 通过ClassLoader.loadClass()方法动态加载类,不会执行初始化块。

类加载实例:当在命令行下执行:java HelloWorld(HelloWorld是含有main方法的类的Class文件),JVM会将HelloWorld.class加载到内存中,并在堆中形成一个Class的对象HelloWorld.class。

基本的加载流程如下:

  1. 寻找jre目录,寻找jvm.dll,并初始化JVM;
  2. 产生一个Bootstrap Loader(启动类加载器);
  3. Bootstrap Loader自动加载Extended Loader(标准扩展类加载器),并将其父Loader设为BootstrapLoader。
  4. Bootstrap Loader自动加载AppClass Loader(系统类加载器),并将其父Loader设为ExtendedLoader。
  5. 最后由AppClass Loader加载HelloWorld类。

类加载器API:
java.lang.ClassLoader 类提供的几个关键方法:
loadClass: 此方法负责加载指定名字的类,首先会从已加载的类中去寻找,如果没有找到;从parent ClassLoader[ExtClassLoader]中加载;如果没有加载到,则从Bootstrap ClassLoader中尝试加载(findBootstrapClassOrNull方法), 如果还是加载失败,则抛出异常ClassNotFoundException, 在调用自己的findClass方法进行加载。如果要改变类的加载顺序可以覆盖此方法;如果加载顺序相同,则可以通过覆盖findClass方法来做特殊处理,例如:解密,固定路径寻找等。当通过整个寻找类的过程仍然未获取Class对象,则抛出ClassNotFoundException异常。如果类需要resolve,在调用resolveClass进行链接。
findLoadedClass 此方法负责从当前ClassLoader实例对象的缓存中寻找已加载的类,调用的为native方法。
findClass 此方法直接抛出ClassNotFoundException异常,因此要通过覆盖loadClass或此方法来以自定义的方式加载相应的类。
findSystemClass 此方法是从sun.misc.Launcher$AppClassLoader中寻找类,如果未找到,则继续从BootstrapClassLoader中寻找,如果仍然未找到,返回null
defineClass 此方法负责将二进制字节流转换为Class对象,这个方法对于自定义类加载器而言非常重要。如果二进制的字节码的格式不符合jvm class文件格式规范,则抛出ClassFormatError异常;如果生成的类名和二进制字节码不同,则抛出NoClassDefFoundError;如果加载的class是受保护的、采用不同签名的,或者类名是以java.开头的,则抛出SecurityException异常。

常见异常:

  1. ClassNotFoundException 这是最常见的异常,产生这个异常的原因为在当前的ClassLoader中加载类时,未找到类文件,
  2. NoClassDefFoundError 这个异常是因为加载到的类中引用到的另外类不存在,例如要加载A,而A中盗用了B,B不存在或当前的ClassLoader无法加载B,就会抛出这个异常。
  3. LinkageError该异常在自定义ClassLoader的情况下更容易出现,主要原因是此类已经在ClassLoader加载过了,重复的加载会造成该异常。
0 0
原创粉丝点击