Java源码分析——Class类、ClassLoader类解析(三) 类加载器、实现自定义类加载器

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/83474194

    在这个系列的第一篇章就讲解了Class类的获取以及加载过程,但是并没有提及具体的加载过程,在java中,加载一个类是通过ClassLoader类来执行的,也就是类加载器完成。java中所有的类,都必须加载进jvm中才能运行,这个加载的意思是指将.class文件加载进jvm中,返回一个Class类对象的过程。

    在官方的定义中,类加载器的定义如下:

类加载器(class loader)是一个负责加载JAVA类(classes)的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,class loader尝试定位或者产生一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。

    接下来讲解ClassLoader类里几个重要的方法,在类的抽象与获取里,简单的讲解了loadClass方法与forName方法的异同,现在来详细的对它分析,它的源码如下:

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) {
                        c = parent.loadClass(name, false);
                    } else {
                    //否则,调用jvm内置加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                //没找到。抛出异常
                }
				//两者都没找到
                if (c == null) {
                    long t1 = System.nanoTime();
                    //抛出异常
                    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;
        }
    }

    在方法的首先,便调用getClassLoadingLock方法来获取该类的锁,实现同步,而让该方法线程是安全的,接着调用findLoadedClass方法来判断该类是否被加载过, 如加载过则直接返回该Class类对象。接着会判定该加载器的父加载器是否有,如果有则调用父加载器加载,没有则直接调用jvm中的内置加载器加载。接着是判断是否需要连接与该类相关的类,也就是resolveClass方法,该方法的作用是加载的不指定的类以及该类引用的所有其他类时。

    而另外一个比较重要的方法是defineClass方法,该方法的作用是把.class文件中的二进制流数据加载进内存的,并返回Class类对象。也就是加载的实质性操作是这个方法在进行的,其实现都是在jvm中,也就是说是native方法,java提供了三种defineClass方法,其最后一种是采用了NIO流:

    private native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd);

    private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);

    private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
                                         int off, int len, ProtectionDomain pd,
                                         String source);

    name是指定的类的完全限定名,b指的是该类的.class文件的二进制流,off指的是开始加载前的指针偏移量,len指的是.class文件的数据大小,source指的是加载的资源,pd指的是权限的检查。

    在java程序中,类有三种,系统类、扩展类、程序员自定义的类,所以类加载器也有对应的三种:

  1. BootstrapClassLoader加载器:该加载器是最顶层的加载器,在jvm中定义且执行,它负责加载%JRE_HOME%\lib目录下rt.jar、resources.jar、charsets.jar等包中的类。
  2. Extention ClassLoader加载器:该加载器是加载拓展的类,加载%JRE_HOME%\lib\ext目录下的类。
  3. Appclass Loader加载器:也称为SystemAppClass加载器,加载当前应用的CLASSPATH目录下的所有类。

    那我们自定义的类由谁加载呢?是由AppclassLoader加载器加载的,因为我们自定义的类就位于CLASSPATH目录下,而其他两种类加载器是主要加载jre有关的类的。有了三种类加载器,那么自然的就有了一个顺序来执行不同的类加载器加载类,因为程序并不会识别你是哪一种类,最简洁的方式就是都试一遍。在sun.misc.Launcher类里面,也就是jvm的入口里:

 private static String bootClassPath =System.getProperty("sun.boot.class.path");
    public static Launcher getLauncher() {
        return launcher;
    }
    
public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        
		......

    }

    从代码中可以看出,首先一个静态的方法获取了BootstrapClassLoader加载器,接着获取ExtClassLoader加载器,最后才获取AppClassLoader加载器。也就是说BootstrapClassLoader加载器是最先被执行的,最后执行的是AppClassLoader加载器。

    它们之间的关系可以用如下图展示:
在这里插入图片描述

    因为BootstrapClassLoader加载器是在jvm中定义的,所以这里不展示了,下面展示一下获取类的类加载器代码:

class Kt{

}

public  class Test {
    public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
      System.out.println("系统类加载器:");
      System.out.println(ClassLoader.getSystemClassLoader());
      System.out.println("自定义类加载器加载器:");
      System.out.println(Kt.class.getClassLoader());
      System.out.println("自定义类加载器的父加载器:");
      System.out.println(Kt.class.getClassLoader().getParent());
      System.out.println("自定义类加载器的父加载器的父加载器:");
   	  System.out.println(Kt.class.getClassLoader().getParent().getParent());
    }
}

在这里插入图片描述

    从结果看出,自定义的类以及系统默认的加载器是AppClassLoader加载器,但是在这里有个困惑,怎么AppClassLoader加载器的父加载器是ExtClassLoader加载器呢?明明两者都是直接在上图中没有继承关系的。因为父加载器不是父类,在类加载器的协调中使用了委托模式。什么意思呢?就是AppClassLoader加载器的父加载器只是它的委托者,当AppClassLoader加载器发现这个类不是它所加载的类的时候,就会委托给它的父加载器ExtClassLoader加载器,当ExtClassLoader加载器假如也发现这个类不是它所加载的类的时候,也会委托给它的父加载器,但是从上面的例子代码中可以看到,ExtClassLoader加载器的父加载器是null,但是别忘了BootstrapClassLoader加载器,它就是ExtClassLoader加载器的父加载器,因为BootstrapClassLoader加载器是有jvm来定义执行的,所以ExtClassLoader加载器的父加载器会显示为空。当这三者都没找到对应类的.class文件时,就会抛出对应.class找不到异常。也就是说,每个加载器都有父加载器。一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。这种委托模式在loadClass方法就有体现:

图片来源
在这里插入图片描述

    那么如何自定义一个类加载器呢?其实很简单,继承ClassLoader类,然后只要重写findClass方法,并在findClass方法里调用defineClass方法就可以了。在讨论loadClass方法时,指出findClass就是个在所有加载器找不到的情况下才调用它,且抛出异常的方法,这里找不到的情况是指java把指定的几个文件下找了一遍都没找到的情况。所以我们可以自定义一个类的存放文件目录,然后在findClass方法里指定我们自定义的类存放文件夹,然后调用definClass来获取Class类对象并创建实例。

  1. 创建一个测试类,并编译形成.class文件,将.class文件放在自定义的目录下,这里我放在:
//目录:/home/image
//测试类
public  class Cat {
    public void print(){
        System.out.println("已经被调用啦");
    }
}
  1. 创建一个类继承ClassLoader并重写findClass:
public class MyClassLoader extends ClassLoader{
    private String root;
    public MyClassLoader(String  root){
        this.root=root;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String filename=getFileName(name);
        File file = new File(root,filename);
        try {
            //将数据从.class文件中读出来并存入byte数组
            FileInputStream is = new FileInputStream(file);
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            int len = 0;
            try {
                while ((len = is.read()) != -1) {
                    bos.write(len);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            byte[] data = bos.toByteArray();
            is.close();
            bos.close();
            //调用defineClass方法加载.class文件
            return defineClass(name,data,0,data.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //失败调用父类findClass方法
        return super.findClass(name);
    }
    //获取.class名
    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        if(index == -1){
            return name+".class";
        }else{
            return name.substring(index+1)+".class";
        }
    }
}
  1. 用反射的方式调用对应的方法测试:
public  class Test {
    public static void main(String args[]) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        MyClassLoader classLoader=new MyClassLoader("/home/image");
        //这里依旧需要加载类的相对路径
        Class cat=classLoader.loadClass("test.Cat");
        Cat test=(Cat) cat.newInstance();
        test.print();
    }
}
//打印输出为:已经被调用啦

    可见确实成功的调用了。

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/83474194