ClassLoader工作机制

本人在研究生阶段做JavaWeb开发时,当把项目发布到Tomcat上运行时,曾遇到过一个神奇的问题,这个问题由“CLassCastException”异常引起,当然这个异常很常见,其实我要说的神奇之处在于------我明明使用的是这个类本身new的对象,然后编译没有问题,但运行时却抛出这个异常。类似这样:MyClass mc=new MyClass(),异常就发生在这一句,是不是很奇怪了。为什么会类型转换失败?

一般来说,产生ClassCastException的主要原因有以下两点:
        1.类不兼容,当应用程序代码尝试将某一对象转换为某一子类时,如果该对象并非该子类的实例,JVM就会抛出ClassCastException异常。
        2.类兼容,但加载时使用了不同的ClassLoader。而我曾碰到的问题就源于第二种情况,这种情况一般不会引起注意,当发生这种情况时,又让人委迷茫,因为我已经确信是用这个类创建的对象!原因就是因为同一个类使用了不同的ClassLoader(Tomcat容器与JDK类加载器的冲突)导致的

鉴于这种情况的隐蔽性,今天特此分享一下ClassLoader的工作机制和原理方面的问题

Java是具有动态性,什么是动态性?有个最直观的例子:windows系统的即插即用,支持即插即用的设备可以在系统不重新启动的情况下既可以热把插使用。而java的动态性表现在:我们的程序可以不用全盘的重新编译就能对程序某部分进行更新,C#也和java一样具有动态性,而且它的这种动态性表现更为直观:直接生成windows的动态连接库文件------dll文件。而java生成的是class文件,class是怎么实现动态性的了,这个时候就全靠我们今天的主角:java的类加载器。

我们都知道所有的java类都是继承了object这个类,在object这个类中有一个方法:getclass(),这个方法返回的是一个Class类对象。

一旦一个类被载入JVM中,同一个类就不会被再次载入了(切记,同一个类)。这里存在一个问题就是什么是“同一个类”?正如一个对象有一个具体的状态,即标识,一个对象始终和其代码(类)相关联。同理,载入JVM的类也应该有一个具体的标识,我们知道:在JAVA中,一个类用其完全匹配类名(fullyqualifiedclassname)作为标识,这里指的完全匹配类名是包名和类名。不过在JVM中一个类是用其全名再附加上一个加载类ClassLoader的实例作为唯一标识。因此,如果一个名为Pg的包中,有一个名为Cl的类,被类加载器KlassLoader的一个实例对象kl1加载,生成Cl的对象,即C1.class(这里指类,而非对象)在JVM中表示为(Cl,Pg,kl1)。这意味着两个类加载器的实例(Cl,Pg,kl1)和(Cl,Pg,kl2)是不同的,被它们所加载的类也因此完全不同,互不兼容的。

在java中每个类都是由某个类加载器的实体来载入的,因此在Class类的实体中,都会有字段记录载入它的类加载器的实体(当为null时,其实是指BootstrapClassLoader)。在java类加载器中除了引导类加载器(既BootstrapClassLoader),所有的类加载器都有一个父类加载器(因为他们本身自己就是java类)。而类的加载机制是遵循一种委托模式:当类加载器有加载类的需求时,会先请求其Parent加载(依次递归),如果在其父加载器树中都没有成功加载该类,则由当前类加载器加载。

java的类加载器分为以下几种:

1,BootstrapClassLoader,用C++实现,一切的开始,是所有类加载器的最终父加载器。负责将一些关键的Java类,如java.lang.Object和其他一些运行时代码先加载进内存中。

2,ExtClassLoader,用java实现,是Launcher.java的内部类,编译后的名字为:Launcher$ExtClassLoader.class。此类由BootstrapClassLoader加载,但由于BootstrapClassLoader已经脱离了java体系(c++),所以Launcher$ExtClassLoader.class的Parent(父加载器)被设置为null;它用于装载Java运行环境扩展包(jre/lib/ext)中的类,而且一旦建立其加载的路径将不再改变。

3,AppClassLoader,用java实现,也是是Launcher.java的内部类,编译后的名字为:Launcher$AppClassLoader.class。AppClassLoader是当BootstrapClassLoader加载完ExtClassLoader后,再被BootstrapClassLoader加载。所以ExtClassLoader和AppClassLoader都是被BootstrapClassLoader加载,但AppClassLoader的Parent被设置为ExtClassLoader。可见Parent和由哪个类加载器来加载不一定是对应的。

个类装载器是我们经常使用的,可以调用ClassLoader.getSystemClassLoader()来获得,如果程序中没有使用类装载器相关操作设定或者自定义新的类装载器,那么我们编写的所有java类都会由它来装载。而它的查找区域就是我们常常说到的Classpath,一旦建立其加载路径也不再改变。

4,ClassLoader:一般我们自定义的ClassLoader从ClassLoader类继承而来。比如:URLClassloader是ClassLoader的一个子类,而URLClassloader也是ExtClassLoader和AppClassLoader的父类(注意不是父加载器)。

让我们先回到java的动态性,实现java的动态性有两种方法类型:一种是隐式,另一种是显式。什么是隐式?new 这个关键字我们都认识,当我们用其将类实例化时(即将对象载入),这种就是隐式!我们再来看显式的实现方法,一种可以由java.long.Class里面的forName()方法将类实例化,其中也用到了类加载器,另一种是由也就是直接用类加载器ClassLoader来实现。

ClassLoader一些重要的方法

A) 方法 loadClass

ClassLoader.loadClass() 是 ClassLoader 的入口点。该方法的定义如下:

Class loadClass( String name, boolean resolve );

参数name JVM 需要的类的名称,如Person 或 java.lang.Object。

参数 resolve 参数告诉方法是否需要解析类。在准备执行类之前,应考虑类解析。并不总是需要解析。如果 JVM 只需要知道该类是否存在或找出该类的超类,那么就不需要解析。

B) 方法 defineClass

defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。defineClass 管理 JVM 的许多复杂、神秘和倚赖于实现的方面 -- 它把字节码分析成运行时数据结构、校验有效性等等。不必担心,您无需亲自编写它。事实上,即使您想要这么做也不能覆盖它,因为它已被标记成final的。

C) 方法 findSystemClass

findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。(Java 2 中 ClassLoader 的变动提供了关于 Java 版本 1.2 这个过程变动的详细信息。)对于定制的 ClassLoader,只有在尝试其它方法装入类之后,再使用 findSystemClass。原因很简单:ClassLoader 是负责执行装入类的特殊步骤,不是负责所有类。例如,即使 ClassLoader 从远程的 Web 站点装入了某些类,仍然需要在本地机器上装入大量的基本 Java 库。而这些类不是我们所关心的,所以要 JVM 以缺省方式装入它们:从本地文件系统。这就是findSystemClass 的用途。

D) 方法 resolveClass

正如前面所提到的,可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用resolveClass,这取决于 loadClass 的 resolve 参数的值。

E) 方法 findLoadedClass

findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法。

怎么组装这些方法

1)调用 findLoadedClass 来查看是否存在已装入的类。

2)如果没有,那么采用那种特殊的神奇方式来获取原始字节。

3)如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。

posted @ 2016-07-07 10:51  一人浅醉-  阅读(1504)  评论(0编辑  收藏  举报