深入理解JVM中的ClassLoader

2017-12-07 发泄吧Net网

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

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);
	}
	
}

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

为什么有的类的加载器为null?

点此查阅jdk文档

我们可以查阅jdk中的函数说明,发现有这么一段话:

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。

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

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

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

类加载的委托机制原则

  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的安装目录

如何验证前面的结论?

很多人学习类加载器只是浏览一遍文档结束,很难有深刻的映像,时间一久就忘记,所以下面给出一个例子,可以加深对类加载器委托机制的印象

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

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

简单来说,一句话概括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


用户评论
开源开发学习小组列表