在Java虚拟机中,负责査找并加载类的那部分被称为类加载器子系统。从Java虚拟机的角度来讲,只存在两种不同的类加载器: 一种是启动类加载器(Eootstrap ClassLoader), 这个类加载器使用 C++语言实现,是虚拟机自身的一部分;另外一种就是所有其他的类加载器, 这些类加载器都由Java语言实现, 独立于虚拟机外部, 并且全都继承自抽象装 java.lang.ClassLoader。从Java开发人员的角度来看,类加载器就还可以划分得更细致一些, 绝大部分Java程序都会使用到以下三种系统提供的类加载器:启动类加载器(BootStrap)、扩展类加载器(Extension)、系统类加载器(System)。
Java中的类加载器实质上也是类,功能是把类载入JVM中,由不同的类加载器加载的类将被放在虚拟机内部的不同命名空间中。
类加载器子系统涉及Java虚拟机的其他几个组成部分, 以及几个来自java.Iang库的类。 比如, 用户自定义的类加载器是普通的Java对象, 它的类必须派生自java.lang.ClassLoader类。ClassLoader中定义的方法为程序提供了访问类加载器机制的接口 。 此外, 对于每一个被加载的类型, Java虚拟机都会为它创建一个java.lang.Class类的实例来代表该类型。和所有其他对象一 样, 用户自定义的类加载器以及Class类的实例都放在内存中的堆区, 而加载的类型信息则都位于方法区。
启动类加载器(Bootstrap ClassLoader)
启动类加载器负责将存放在<JAVA_HOME>\lib目录中的, 或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名称,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载) 类库加载到虚拟机内存中 。启动类加载器无法被 Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,就直接使用null代替。
扩展类加载器(Extension ClassLoader)
这个加载器由 sun.misc.Launcher$ExtClassLoader实现, 它负责加载<JAVA_HOME>\lib\ext目录中的, 或者被java.ext.dirs 系统变量所指定的路径中的所有类库 , 开发者可以直接使用扩展类加载器。
应用程序类加载器(Application ClassLoader)
这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于这个类加载器是 ClassLoader中的getSystemClassLoader( )方法的返回值, 所以一般也称它为系统类加载器。它负责加载用户类路径 (ClassPath)上所指定的类库,开发者可以直接使用这个类加载器, 如果应用程序中没有自定义过自已的装加载器, 一般情况下这个就是程序中默认的类加载器。
类加载机制
JVM的类加载机制主要有如下三种:
- 全盘负责: 所谓全盘负责, 就是说当一个类加载器负责加载某个 CIass的时候, 该 Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
- 父类委托:所谓父类委托则是先让 parent(父)类加载器试图加载该 Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制: 缓存机制将会保证所有被加载过的 Class 都会被缓存, 当程序中需要使用某个CIass时,类加载器先从缓存中搜寻该 Class, 只有当缓存中不存在该 Class对象时, 系统才会读取该类对应的二进制数据,并将其转换成Class对象存入缓存区。这就是为什么修改了 Class后, 程序必须重启动JVM,所作的修改才会生效的原因。
除了可以使用Java提供的类加载器之外,也可以自定义继承 ClassLoader的加载器,JVM中这四种加载器的层次结构如图:
双亲委派模型
既然类加载器不止一个,那么一个类需要加载时,它们之间是如何协调工作的,即java是如何区分一个类该由哪个类加载器来完成呢? 我们使用的就是父类委托机制,也就是常说的双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自已的父类加载器, 这里类加载器之间的父子关系一般不会以继承(lnheritance)的关系来实现,而是都使用组合(Composition)关系来复用类加载器的代码 。
其工作过程是:如果一个类加载器收到类加载请求,它首先不会自己去尝试加载这个类, 而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此, 因此所有的类加载请求最终都直接传送到顶层的启动类加载器中, 只有当父加载器反馈自己无法完成这个加载请求 (它的搜素范围中没有找到所需的类) 时, 子加载器才会尝试自己去加载。
用户自定义类加载器
Java应用程序能够在运行时安装用户自定义的类加载器, 这种类加载器能够使用自定义的方式来加载类,用户自定义的类加载器能够用Java编写, 能够被编译为class文件, 能够被虚拟机装载, 还能够像其他对象一样实例化。 它们实际上只是运行中的Java应用程序可执行代码的一部分。
由于有用户自定义类加载器, 所以不必在编译的时候就知道运行中的Java应i用程序中最终会加入的所有的类。 用户自定义的类加载器使得在运行时扩展Java应用程序成为可能。当它运行时, 应用程序能够决定它需要哪些额外的类, 能够决定是使用一个或是更多的用户定义的类加载器来加载。由于类加载器是使用Java编写的,所以能用任何在Java代码中可以表述的风格来进行类的装载。这些类可以通过网络下载, 可以从某些数据库中获取, 甚至可以动态生成 。
每一个类被加载的时候, Java虚拟机都监视这个类, 看它到底是被启动类加载器还是被用户自定义类装载器装载。 当被加载的类引用了另外一个类时, 虚拟机就会使用加载第一个类的类加载器加载被引用的类。
尽管“用户自定义类加载器''本身是Java程序的一部分, 但类ClassLoader中的四个方法是通往Java虚拟机的通道:
// Four of the methods declared in class java.lang.ClassLoader
protected final Class defineClass(String name, byte data[], int offset, int length);
protected final Class defineClass(String name, byte data[], int offset, int length, ProtectionDomain protectionDomain );
protected final Class findSystemClass(String name);
protected final Class resolveClass(Class c);
任何Java虚拟机实现都必须把这些方法连接到内部的类装载器子系统中 。
两个被重载的defineClass( )方法都要接受一个名为data[]的字节数组作为输入参数,并且在data[offset]到data[offset+length]之间的二进制数据必须符合Java class文件格式——它表示一个新的可用类。 而name参数是个字符串, 它指出该类的全限定名 。 当使用第一个defineClass( )时,该类型将被赋以默认的保护域。使用第二个defineClass( )时,该类型的保护域将由它的protectionDomain参数指定。每个Java虚拟机实现都必须保证ClassLoader类的defineClass( )方法能够把新类型导入到方法区中。
findSystemClass( )方法接受一个字符串作为参数,它指出将被装入类型的全限定名。在版本1.0和版本1.1中,这个方法会通过启动类加载器来加载指定类型。如果启动类加载器装载完成,它会返回对CIass对象(该对象描述了该类型)的引用。如果没有找到相应的class文件,它会抛出ClassNotFoundException异常。在版本1.2中, findSystemClass( )方法使用系统类加载器来装载指定类型。任何Java虚拟机实现都必须保证findSystemClass( )方法能够以这种方式调用启动类装载器(如果运行版本1.0或版本1.1 ),或者系统类装载器(如果运行版本1.2或以上)。
resolveClass( )方法接受一个Class实例的引用作为参数,它将对该Class实例表示的类型执行连接动作。而前面提到的defineClass( )方法则只负责加载。当defineClass( )方法返回一个CIass实例时, 也就表示指定的class文件已经被找到并装载到方法区了, 但是却不一定被连接和初始化。 Java虚拟机实现必须保证ClassLoader类的resolveClass( )方法能够让类加载器子系统执行连接动作。
命名空间
每个类加载器都有自己的命名空间, 其中维护着由它装载的类。所以一个Java程序可以多次装载具有同一个全限定名的多个类。 这样一个类的全限定名就不足以确定在一个Java虚拟机中的唯一性。 因此, 当多个类加载器都装载了同名的类时, 为了唯一地标识该类型,还要在类型名称前加上装载该类型(指出了它所位于的命名空间)的类装载器的标识。
Java虚拟机中的命名空间, 其实是解析过程的结果 。 对于每一个被加载的类型, Java虚拟机都会记录装载它的类加载器。 当虚拟机解析一个类到另一个类的符号引用时, 它需要被引用类的类加载器。