Java虚拟机中的类加载器

一、概述
JVM设计者把类加载阶段中的“通过'类全名'来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。

二、双亲委派模型
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。
从Java开发人员的角度来看,大部分Java程序一般会使用到以下三种系统提供的类加载器:
1)启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
2)扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\,该加载器可以被开发者直接使用。
3)应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
我们的应用程序都是由这三类加载器互相配合进行加载的,我们也可以加入自己定义的类加载器。这些类加载器之间的关系如下图所示:

如上图所示的类加载器之间的这种层次关系,就称为类加载器的双亲委派模型(Parent Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承(Inheritance)的关系来实现,而是通过组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。
在rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程的代码,具体源码如下:
protected synchronized Class loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  
    // 首先检查该name指定的class是否有被加载  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        try {  
            if (parent != null) {  
                // 如果parent不为null,则调用parent的loadClass进行加载  
                c = parent.loadClass(name, false);  
            } else {  
                // parent为null,则调用BootstrapClassLoader进行加载  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果仍然无法加载成功,则调用自身的findClass进行加载  
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
}  
通过上面代码可以看出,双亲委派模型是通过loadClass()方法来实现的,根据代码以及代码中的注释可以很清楚地了解整个过程其实非常简单:先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。

3.自定义类加载器
若要实现自定义类加载器,只需要继承java.lang.ClassLoader 类,并且重写其findClass()方法即可。java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等,ClassLoader 中与加载类相关的方法如下:
方法                                 说明
getParent()              返回该类加载器的父类加载器。
loadClass(String name)   加载名称为二进制名称为name的类,返回的结果是java.lang.Class 类的实例。
findClass(String name)   查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。
findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。
resolveClass(Class<?> c) 链接指定的 Java 类。
注意:在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。
在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。例子代码如下:
/** 
     * 一、ClassLoader加载类的顺序 
     *  1.调用 findLoadedClass(String) 来检查是否已经加载类。 
     *  2.在父类加载器上调用 loadClass 方法。如果父类加载器为 null,则使用虚拟机的内置类加载器。 
     *  3.调用 findClass(String) 方法查找类。 
     * 二、实现自己的类加载器 
     *  1.获取类的class文件的字节数组 
     *  2.将字节数组转换为Class类的实例 
     */  
    public class ClassLoaderTest {  
        public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException {  
            //新建一个类加载器  
            MyClassLoader cl = new MyClassLoader("myClassLoader");  
            //加载类,得到Class对象  
            Class<?> clazz = cl.loadClass("classloader.Animal");  
            //得到类的实例  
            Animal animal=(Animal) clazz.newInstance();  
            animal.say();  
        }  
    }  
    class Animal{  
        public void say(){  
            System.out.println("hello world!");  
        }  
    }  
    class MyClassLoader extends ClassLoader {  
        //类加载器的名称  
        private String name;  
        //类存放的路径  
        private String path = "E:\\workspace\\MyTest\\src";  
        MyClassLoader(String name) {  
            this.name = name;  
        }  
        MyClassLoader(ClassLoader parent, String name) {  
            super(parent);  
            this.name = name;  
        }  
        /** 
         * 重写findClass方法 
         */  
        @Override  
        public Class<?> findClass(String name) {  
            byte[] data = loadClassData(name);  
            return this.defineClass(name, data, 0, data.length);  
        }  
        public byte[] loadClassData(String name) {  
            try {  
                name = name.replace(".", "//");  
                FileInputStream is = new FileInputStream(new File(path + name + ".class"));  
                ByteArrayOutputStream baos = new ByteArrayOutputStream();  
                int b = 0;  
                while ((b = is.read()) != -1) {  
                    baos.write(b);  
                }  
                return baos.toByteArray();  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
            return null;  
        }  
    }  

4.动态加载Jar和ClassLoader 隔离问题
1)动态加载Jar
Java 中动态加载 Jar 比较简单,如下:
URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};  
URLClassLoader loader = new URLClassLoader(urls, parentLoader);  
表示加载 libs 下面的 jar1.jar,其中 parentLoader 就是上面1中的 parent,可以为当前的 ClassLoader。
2)ClassLoader 隔离问题
一个运行程序中有没有可能同时存在两个包名和类名完全一致的类?
JVM 及 Dalvik 对类唯一的识别是 ClassLoader id + PackageName + ClassName,所以一个运行程序中是有可能存在两个包名和类名完全一致的类的。并且如果这两个”类”不是由一个 ClassLoader 加载,是无法将一个类的示例强转为另外一个类的,这就是 ClassLoader 隔离。 
我们可以通过 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一样。
3)加载不同 Jar 包中公共类
假设工程包含了 common.jar, jar1.jar, jar2.jar,并且 jar1.jar 和 jar2.jar 都包含了 common.jar,我们通过 ClassLoader 将 jar1, jar2 动态加载进来,这样在工程中实际是存在三份 common.jar,我们怎么保证 common.jar 只有一份而不会造成上面3中提到的 ClassLoader 隔离的问题呢,其实很简单,在生成 jar1 和 jar2 时把 common.jar 去掉,只保留一份,以 当前工程的ClassLoader 为 parentClassLoader 即可。 
那么我们能不能自己写个类叫java.lang.System?
答案:通常不可以,但可以采取另类方法达到这个需求。 
解释:为了不让我们写System类,类加载采用委托机制,这样可以保证爸爸们优先,爸爸们能找到的类,儿子就没有机会加载。而System类是Bootstrap加载器加载的,就算自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载。但是,我们可以自己定义一个类加载器来达到这个目的,为了避免双亲委托机制,这个类加载器也必须是特殊的。由于系统自带的三个类加载器都加载特定目录下的类,如果我们自己的类加载器放在一个特殊的目录,那么系统的加载器就无法加载,也就是最终还是由我们自己的加载器加载。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值