JVM之类加载器

一. 类加载器的父亲委托机制

类加载器用来把类加载到Java虚拟机中。从JDK1.2开始,类的加载过程采用父亲委托机制,这种机制更好的保证java平台的安全。在此委托机制中,除了java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当java程序请求加载器loader加载一个类时,loader首先委托自己的父加载器去加载该类,若加载器能加载,则由父加载器完成加载任务,否则由加载器loader本身加载该类。若所有的父加载器以及loader本身都不能加载,则跑出ClassNotFoundException异常。
java虚拟机自带了以下几种加载器。

  • 根类加载器(Bootstrap):该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。根类加载器的实现依赖于底层操作系统,属于虚拟机的实现的一部分,它并没有继承java.lang.ClassLoader类。
  • 扩展类加载器(Extension):它的父加载器为根类加载器。它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加载类库,如果把用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯java类,是java.lang.ClassLoader类的子类。
  • 系统类加载器(System):也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父类加载器。系统类加载器是纯java了,是java.lang.ClassLoader类的子类。
    这里写图片描述

若有一个类加载器能成功加载某个类,那么这个类加载器被称为该类的定义类加载器,所有能成功返回Class对象的引用的类加载器(包括定义类加载器)都被称为该类的初始类加载器
父子加载器并非继承关系,也就是说子加载器不一定是继承了父加载器。加载器之间的父子关系实际上指的是加载器对象之间的包装关系,而不是类之间的继承关系。一对父子加载器可能是同一个加载器类的两个实例,也可能不是。在子加载器对象中包装了一个父加载器对象。例如文章最后的一个示例loader1和loader2都是MyClassLoader类的实例,并且loader2包装了loader1,那么loader1是loader2的父加载器。
父亲委托机制的优点是能够提高软件系统的安全性。应为在此机制下,用户自定义的类加载器不可能加载应该由父加载器加载的可靠类,从而防止不可靠甚至恶意的代码代替由父加载器加载的可靠代码。例如,java.lang.Object类总是由根类加载器加载,其他任何用户自定义的类加载器都不可能加载含有恶意代码的java.lang.Object类。
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所有加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
由同一个类加载器加载的属于相同宝的类组成了运行时包。决定两个类是不是属于同一个运行是包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一运行是包的类才能相互访问包可见(即默认的访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。假如用户自己定义了一个类java.lang.Spy,并由用户自定义的类加载器加载,由于java.lang.Spy和核心类库java.lang.*由不同的加载器加载,它们属于不同的运行时包,所以java.lang.Spy不能访问核心类库java.lang包中的包可见成员。

下面看一个例子:

public class Test {
}

public class TestMain {
    public static void main(String[] args) {

        ClassLoader loader = Test.class.getClassLoader();
        while (loader!=null){
            System.out.println(loader);
            loader = loader.getParent();
        }
    }
}

上面这个程序运行结果:
这里写图片描述
从结果我们可以看出,默认情况下,用户自定义的类使用 AppClassLoader 加载,AppClassLoader 的父加载器为 ExtClassLoader,但是 ExtClassLoader 的父加载器却显示为空,这是什么原因呢?究其缘由,启动类加载器属于 JVM 的一部分,它不是由 Java 语言实现的,在 Java 中无法直接引用,所以才返回空。但如果是这样,该怎么实现 ExtClassLoader 与 启动类加载器之间双亲委派机制?我们可以参考一下源码:

protected Class<?> loadClass(String name, boolean resolve)
       throws ClassNotFoundException
   {
       synchronized (getClassLoadingLock(name)) {
           // First, check if the class has already been loaded
           Class<?> c = findLoadedClass(name);
           if (c == null) {
              long t0 = System.nanoTime();
               try {
                   if (parent != null) {
                       c = parent.loadClass(name, false);
                   } else {
                       c = findBootstrapClassOrNull(name);
                   }
               } catch (ClassNotFoundException e) {
                   // ClassNotFoundException thrown if class not found
                   // the non-null parent class loader
               }

               if (c == null) {
                   // If still not found, then invoke findClass in order
                   // to find the class.
                   long t1 = System.nanoTime();
                   c = findClass(name);

                   // this is the defining class loader; 
                   sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                                   sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);         
                   sun.misc.PerfCounter.getFindClasses().increment();
               }
           }
           if (resolve) {
               resolveClass(c);
           }
           return c;
       }
   }

 从源码可以看出,ExtClassLoader 和 AppClassLoader都继承自 ClassLoader 类,ClassLoader 类中通过 loadClass 方法来实现双亲委派机制。整个类的加载过程可分为如下三步:

  1、查找对应的类是否已经加载。

  2、若未加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类去加载,否则调用启动类加载器加载(findBootstrapClassOrNull 再往下会调用一个 native 方法)。

  3、若第二步加载失败,则调用当前类加载器加载。

  通过上面这段程序,可以很清楚的看出扩展类加载器与启动类加载器之间是如何实现委托模式的。

  现在,我们再验证另一个问题。我们将刚才的Test类打成jar包,将其放置在 <Java_Runtime_Home>\lib\ext 目录下,然后再次运行上面的代码,结果如下:

这里写图片描述
现在,该类就不再通过 AppClassLoader 来加载,而是通过 ExtClassLoader 来加载了。如果我们试图把jar包拷贝到< Java_Runtime_Home >\lib,尝试通过启动类加载器加载该类时,我们会发现编译器无法识别该类,因为启动类加载器除了指定目录外,还必须是特定名称的文件才能加载。

1、Bootstrap Loader(启动类加载器):加载System.getProperty(“sun.boot.class.path”)所指定的路径或jar。通过System.out.println(System.getProperty(“sun.boot.class.path”));打印,发现主要是“D:\Program Files\Java\jdk1.6.0_10\jre\lib”中的jar包。

2、Extended Loader(标准扩展类加载器ExtClassLoader):加载System.getProperty(“java.ext.dirs”)所指定的路径或jar。在使用Java运行程序时,也可以指定其搜索路径,例如:java -Djava.ext.dirs=d:\projects\testproj\classes HelloWorld。
通过打印System.out.println(System.getProperty(“java.ext.dirs”));,可以发现主要加载目录为:
“D:\Program Files\Java\jdk1.6.0_10\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext”

3、AppClass Loader(系统类加载器AppClassLoader):加载System.getProperty(“java.class.path”)所指定的路径或jar。在使用Java运行程序时,也可以加上-cp来覆盖原有的Classpath设置,例如: java -cp ./lavasoft/classes HelloWorld
对于eclipse,就是“.classpath”中的jar包。

ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。

二. 自定义类加载器

除了虚拟机自带的加载器以外,用户还可以定制自己的类加载器。java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器应该继承ClassLoader类。当生成一个自定义的类加载器实例时,如果没有指定它的父加载器,那么系统类加载器就将成为该类加载器的父加载器。
要创建用户自己的类加载器,只需要扩展java.lang.ClassLoader类,然后覆盖它的findClass(String name)方法即可,该方法根据参数指定的类名字,返回对应的Class对象的引用。

/**
 * Created by 陈明 on 2015/4/10.
 */
public class MyClassLoader extends ClassLoader {
    private String name;
    private String path = "d:\\";
    private final String fileType = ".class"; //加载文件的扩展名

    public MyClassLoader(String name)
    {
        super();
        this.name = name;
    }

    public MyClassLoader(ClassLoader parent,String name)
    {
        super(parent);
        this.name = name;
    }

    @Override
    public String toString() {
        return this.name;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = this.loadClassData(name);
        System.out.println(data.length);
        return this.defineClass(name, data, 0, data.length);
    }

    private byte[] loadClassData(String name) {
        FileInputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        String pathname = path+"classloader\\"+name+fileType;
        System.out.println(pathname);

        try{
            is = new FileInputStream(new File(pathname));
            System.out.println(is.available());
            baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            int len = 0;
            while((len = is.read(buffer)) != -1){
                baos.write(buffer, 0, len);
            }

            data =  baos.toByteArray();
        }catch (Exception e)
        {
            e.printStackTrace();
        }finally {
            try {
                is.close();
                baos.close();

            }catch (Exception e)
            {
                e.printStackTrace();
            }
        }
        return data;

    }

    public static void main(String[] args) throws Exception{
        MyClassLoader loader1 = new MyClassLoader("loader1");
        MyClassLoader loader2 = new MyClassLoader(loader1, "loader2");
        Class clazz = loader2.loadClass("Sample");//在指定目录下放好Sample.class文件
        clazz.newInstance();

    }

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值