您的位置:首页 > 编程语言 > Java开发

java的类加载器_10

2014-05-07 13:58 316 查看
一,类加载器基本概念

顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成
java.lang.Class
类的一个实例。每个这样的实例用来表示一个
Java 类。通过此实例的 
newInstance()
方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如
Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。基本上所有的类加载器都是 
java.lang.ClassLoader
类的一个实例。


二,java.lang.ClassLoader
类介绍

java.lang.ClassLoader
类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个
Java 类,即 
java.lang.Class
类的一个实例。除此之外,
ClassLoader
还负责加载
Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,
ClassLoader
提供了一系列的方法,比较重要的方法如 表
1所示。关于这些方法的细节会在下面进行介绍。

表 1. ClassLoader 中与加载类相关的方法

方法说明
getParent()
返回该类加载器的父类加载器。
loadClass(String
name)
加载名称为 
name
的类,返回的结果是 
java.lang.Class
类的实例。
findClass(String
name)
查找名称为 
name
的类,返回的结果是 
java.lang.Class
类的实例。
findLoadedClass(String
name)
查找名称为 
name
的已经被加载过的类,返回的结果是 
java.lang.Class
类的实例。
defineClass(String
name, byte[] b, int off, int len)
把字节数组 
b
中的内容转换成
Java 类,返回的结果是 
java.lang.Class
类的实例。这个方法被声明为 
final
的。
resolveClass(Class<?>
c)
链接指定的 Java 类。
三,类加载器的树状组织结构。

引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。

扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。




四,类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。比如一个 Java 类 
com.example.Sample
,编译之后生成了字节代码文件 
Sample.class
。两个不同的类加载器
ClassLoaderA
和 
ClassLoaderB
分别读取了这个 
Sample.class
文件,并定义出两个 
java.lang.Class
类的实例来表示这个类。这两个实例是不相同的。对于
Java 虚拟机来说,它们是不同的类。试图对这两个类的对象进行相互赋值,会抛出运行时异常 
ClassCastException
。下面通过示例来具体说明。代码清单
3中给出了 Java 类 
com.example.Sample


清单 3. com.example.Sample 类


package com.example;

public class Sample {
private Sample instance;

public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}


如 代码清单
3所示,
com.example.Sample
类的方法 
setSample
接受一个 
java.lang.Object
类型的参数,并且会把该参数强制转换成
com.example.Sample
类型。测试
Java 类是否相同的代码如 代码清单
4所示。

清单 4. 测试 Java 类是否相同


public void testClassIdentity() {
String classDataRootPath = "C:\\workspace\\Classloader\\classData";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "com.example.Sample";
try {
Class<?> class1 = fscl1.loadClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.loadClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setSample", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}


代码清单
4中使用了类 
FileSystemClassLoader
的两个不同实例来分别加载类 
com.example.Sample
,得到了两个不同的 
java.lang.Class
的实例,接着通过 
newInstance()
方法分别生成了两个类的对象 
obj1
和 
obj2
,最后通过
Java 的反射 API 在对象 
obj1
上调用方法 
setSample
,试图把对象 
obj2
赋值给 
obj1
内部的 
instance
对象。代码清单
4的运行结果如 代码清单
5所示。

五,测试 Java 类是否相同的运行结果


抛出了 
java.lang.ClassCastException
异常。虽然两个对象 
obj1
和 
obj2
的类的名字相同,但是这两个类是由不同的类加载器实例来加载的,因此不被
Java 虚拟机认为是相同的。

了解了这一点之后,就可以理解代理模式的设计动机了。代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用
java.lang.Object
类,也就是说在运行的时候,
java.lang.Object
这个类需要被加载到
Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 
java.lang.Object
类,而且这些类之间是不兼容的。通过代理模式,对于
Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

不同的类加载器为相同名称的类创建了额外的名称空间。相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间。这种技术在许多框架中都被用到。


六,加载类的过程

在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 
defineClass
来实现的;而启动类的加载过程是通过调用 
loadClass
来实现的。前者称为一个类的定义加载器(defining
loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 
com.example.Outer
引用了类
com.example.Inner
,则由类 
com.example.Outer
的定义加载器负责启动类 
com.example.Inner
的加载过程。

方法 
loadClass()
抛出的是 
java.lang.ClassNotFoundException
异常;方法 
defineClass()
抛出的是
java.lang.NoClassDefFoundError
异常。

类加载器在成功加载某个类之后,会把得到的 
java.lang.Class
类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 
loadClass
方法不会被重复调用。


七,线程上下文类加载器

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 
java.lang.Thread
中的方法 
getContextClassLoader()
setContextClassLoader(ClassLoader
cl)
用来获取和设置线程的上下文类加载器。如果没有通过 
setContextClassLoader(ClassLoader
cl)
方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 
javax.xml.parsers
包中。这些
SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache
Xerces所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 
javax.xml.parsers.DocumentBuilderFactory
类中的 
newInstance()
方法用来生成一个新的
DocumentBuilderFactory
的实例。这里的实例的真正的类是继承自 
javax.xml.parsers.DocumentBuilderFactory
,由
SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 
org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
。而问题在于,SPI
的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

八,一种常用加载类的方法
Class.forName


Class.forName

Class.forName
是一个静态方法,同样可以用来加载类。该方法有两种形式:
Class.forName(String
name, boolean initialize, ClassLoader loader)
和 
Class.forName(String
className)
。第一种形式的参数 
name
表示的是类的全名;
initialize
表示是否初始化类;
loader
表示加载时使用的类加载器。第二种形式则相当于设置了参数 
initialize
的值为 
true
loader
的值为当前类的类加载器。
Class.forName
的一个很常见的用法是在加载数据库驱动的时候。如
Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance()
用来加载
Apache Derby 数据库的驱动。

九,总结
1,java虚拟机使用每一个类的第一件事情就是将该类的字节码装载进来,装载字节码的功能是由装载器来完成的。类装载器负责根据一个类的名称来定位和生成类的字节码数据后返回给java虚拟机。

2,类装载器本身也是一个java类。JVM允许开发人员编写自己的类装载器,以便通过其他各种技术来产生类的字节码

3,不管类装载器采用何方式,只要你能在内存中制造给JVM调用的字节码即可,所以把类装载器描述为类的制造器

4,当一个类被加载后,java虚拟机将其编译的可执行代码存储在内存中,并将索引信息存到HashTable中,索引的KEY为与之对应的类名。

5,java类本身用Class的文件来描述,类装载器装载某个类的字节码的时候其实就是构造Class类的一个实例对象。这个Class类的实例内容正好是当前类字节码的数据。

6,要在程序中获得某个类的字节码数据的Class对象,用对象的实例调用即可new Date().getClass();

7,java类库中提供的一个java.lang.ClassLoader来作为类装载的基础类,JVM调用其loadClass();来加载类,其中是一个抽象类。

8,类装载器本身也需要被另外一个类装载器来装载。

9,JVM中内嵌了一个叫“BootStrap”的类装载器。它属于JVM的内核包中的类(rt.jar中的类)

10,ExitClassLoader类装载器负责加载(jre.lib.ext)目录下的类。AppClassLoader负责加载应用程序的启动类

11,JVM中所有的类转载器都有父子关系的树形结构进行组织,在实例每个类装载器对象时,需要指定一个父级类装载器对象,如果未指定的话则以ClassLoader.getSystemClassLoader()返回的系统类装载器作为其父类的装载器。

12,每个ClassLoader本身只能分别出加载器特定位置和目录中的类,但是其被设计成了委托模式,使得一个ClassLoader可以委托父级类装载器去加载类。

13,要加载一个类时,ClassLoader的loadClass()先看这个类是否被加载,若果没有加载则让其父类去加载,委托一直追溯到BootStrap类,若果都干不了就汇报ClassNotFoundException异常。

14,两个不同的类装载器创建同一个类的字节码数据是两个完全不同的对象,而同一个类的转载器只能加载同一个累的一份字节码数据。

15,采用委托模式避免了JVM中多个类装载器为同一个类创建多个字节码数据的情况,而开发人员自定义的装载器只要覆盖findClass()方法就可以继续采用这种委托模式。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: