深刻理解JVM中的ClassLoader

JVM中的类加载器结构

要理解jvm中的类加载器结构,仅仅查阅文档是不够的。这里给出一个小程序帮助理解jvm虚拟机中的类加载器结构。html

package com.wuyue.demo;

import java.util.Date;
import java.util.List;

/**
 * 测试类
 * @author wuyue
 */
public class JVMClassLoader {

	public static void main(String[] args){
		System.out.println("JVMClassLoader类的加载器的名称:"+JVMClassLoader.class.getClassLoader().getClass().getName());
		System.out.println("System类的加载器的名称:"+System.class.getClassLoader());
		System.out.println("List类的加载器的名称:"+List.class.getClassLoader());
		
		ClassLoader cl = JVMClassLoader.class.getClassLoader();
		while(cl != null){
			System.out.print(cl.getClass().getName()+"->");
			cl = cl.getParent();
		}
		System.out.println(cl);
	}
	
}

复制代码

而后咱们编译并运行这段程序查看下这段代码的运行结果:java

为何有的类的加载器为null?

点此查阅jdk文档c++

咱们能够查阅jdk中的函数说明,发现有这么一段话:bootstrap

Returns the class loader for the class. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class was loaded by the bootstrap class loader.小程序

讲白了,这里的意思就是有的虚拟机实现会用null 来代替bootstrap这个classloader。api

如何理解打印出来的三种类加载器?

BootStrap->ExtClassLoader->AppClassLoader->开发者自定义类加载器. 可认为BootStrap为祖先加载器,开发者自定义类加载器为底层加载器。 不过多数状况,咱们并不会自定义类加载器,因此大多数状况,AppClassLoader就是JVM中的底层类加载器了。数组

注意BootStrap是用c++代码编写的,后面2个类加载器则是java类编写 这就解释了为何BootStrap加载器会返回null了,由于这个祖先类加载器在 java里根本找不到吗bash

类加载的委托机制原则

  1. 由下到上加载,顶层加载不了再交给下层加载,若是回到底层位置加载 还加载不到,那就会报ClassNotFound错误了。
  2. 如同一开始咱们的例子同样,JVMClassLoader 这个类 为何输出的类加载器名称是AppClassLoader呢,缘由就是先找到顶层的Boot类加载器发现找不到这个类,而后继续找ext类加载器仍是找不到,最后在AppClassLoder中找到这个类。因此这个类的加载器就是AppClassLoader了。
  3. 为何System和List类的类加载器是Boot类加载器?由于Boot类加载器加载的默认路径就是/jre/lib 这个目录下的rt.jar 。ext加载器的默认路径是 /jre/lib/ext/*.jar.这2个目录下面固然没法找到咱们的JVMClassLoader类了 注意这里的根目录是你jdk的安装目录

如何验证前面的结论?

不少人学习类加载器只是浏览一遍文档结束,很难有深入的映像,时间一久就忘记,因此下面给出一个例子,能够加深对类加载器委托机制的印象markdown

这里咱们能够看到,我是先将编译好的class文件 打成一个jar包,而后再将这个打好的jar包放到咱们jdk路径下的 /jre/lib/ext/ 这个目录下,前面介绍过这个目录就是ext类加载器要加载的目录,而后再次运行咱们一开始编写好的程序就能够发现,一样是JVMClassLoader这个类,一开始咱们的类加载器是appclassloader后面就变成了extclassloader。到这里应该就对类加载器的委托机制有了深入认识了。oracle

如何评价这种委托机制下的类加载器机制?

简单来讲,一句话归纳jvm中的类加载器机制:

能够用爸爸的钱就绝对不用本身的钱,若是爸爸没有钱,再用本身的, 若是本身仍是没有钱,那么就classnotfound异常

好处就是要加载一个类首先交给他的上级类加载器处理,若是上级类有,就直接拿来用,这样若是以前加载过的类就不须要再次重复加载了。简称:能啃老用爹的钱,为啥要用本身的?

看源码再次加深对类加载器的理解。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 看这个类加载过没有若是加载过就不在继续加载了
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //先看有没有爸爸类加载器若是有就继续“递归”调用loadclass这个方法
                        c = parent.loadClass(name, false);
                    } else {
                        //若是没有爸爸类加载器了,就说明到头了。看看
                        //祖先bootstrap类加载器中有没有
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from 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();
                    //若是没有找到就调用本身的findclass找这个类。
                    c = findClass(name);

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

因此看到在代码里其实就是一个调用parent loadclass的过程,若是parent都找不到就调用本身的findclass方法来找。 和咱们前面的分析是一致的。

有兴趣的同窗能够在jdk目录中找到rt.jar 这个jar包,查看AppClassLoader等系统自带的classLoader的源码,有助于加深理解,这里就再也不过多叙述了

自定义类加载器。

首先咱们定义一个CustomDate类,这个类只重写一下toString方法

package com.wuyue.test;

import java.util.Date;

/**
 * 只是重写了Date的toString方法
 */
public class CustomDate extends Date{

    @Override
    public String toString() {
        return "my cystom date";
    }
}

复制代码

而后写一个简单的classloader,自定义的那种。

package com.wuyue.test;


import java.io.*;

public class MyClassLoader extends ClassLoader{

    String classDir;

    public MyClassLoader() {

    }

    public MyClassLoader(String classDir) {
        this.classDir = classDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String classFile=classDir+"/"+name+".class";
        System.out.println("classFile path=="+classFile);

        try {
            //这个地方咱们只是简单的读取文件流的方式来获取byte数组
            //其实能够尝试将class文件加密之后 这里解密 这样就能够保证
            //这种class文件 只有你写的classloader才能读取的了。
            //其余任何classloader都读取不了 包括系统的。
            byte[] classByte=toByteArray(classFile);
            return defineClass(classByte,0,classByte.length);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }


        return super.findClass(name);
    }

    /**
     * the traditional io way
     *
     * @param filename
     * @return
     * @throws IOException
     */
    public static byte[] toByteArray(String filename) throws IOException, FileNotFoundException {

        File f = new File(filename);
        if (!f.exists()) {
            throw new FileNotFoundException(filename);
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream((int) f.length());
        BufferedInputStream in = null;
        try {
            in = new BufferedInputStream(new FileInputStream(f));
            int buf_size = 1024;
            byte[] buffer = new byte[buf_size];
            int len = 0;
            while (-1 != (len = in.read(buffer, 0, buf_size))) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
            throw e;
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            bos.close();
        }
    }
}

复制代码

最后写一个测试咱们自定义classloader的主程序:

package com.wuyue.test;
import java.util.Date;

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

             Class classDate = new MyClassLoader("/Users/wuyue/IdeaProjects/ClassLoaderTest/out/production/ClassLoaderTest/com/wuyue/test").loadClass("com.wuyue.test.CustomDate");
             Class classDate2 = new MyClassLoader("/Users/wuyue/IdeaProjects/ClassLoaderTest/out/production/ClassLoaderTest/com/wuyue/test").loadClass("CustomDate");
             Date date = (Date) classDate.newInstance();
             System.out.println("date ClassLoader:"+date.getClass().getClassLoader().getClass().getName());
             System.out.println(date);

             Date date2 = (Date) classDate2.newInstance();
             System.out.println("date2 ClassLoader:"+date2.getClass().getClassLoader().getClass().getName());
             System.out.println(date2);
         } catch (Exception e1) {
             e1.printStackTrace();
         }

     }
}

复制代码

而后咱们来看一下程序运行结果:

你们能够看到classdate和classDate2 这2个类,咱们在用classLoader去加载的时候传的参数惟一的不一样就是前者传入了完整的包名,然后者没有。这就致使了前者的classLoader依旧是系统自带的appclassloader 然后者才是咱们自定义的classloader。 缘由:

虽然对于classDate和classDate2来讲,咱们手动指定了她的类加载是咱们自定义的myclassloader,可是根据类加载器的规则,咱们能用父亲的loadclass就确定不会用本身的,而咱们系统类加载器,AppClassLoader要想loadclass成功是须要传入完整的包名的。因此classDate的构造仍是传入了完整的包名,这就是为啥classDate的加载器仍是AppClassLoader,可是classDate2并无传入完整的包名,因此AppClassLoader也是找不到这个CustomDate类的,最后只能交给MyClassLoader这个最底层的,咱们自定义的classloader来load