要自定义自己的类加载器来加载类,需要先对类加载器和类加载机制有一些基本的了解。

1、类加载器

类加载器ClassLoader的作用有两个:

①是用于将class文件加载到JVM。
②是用于判断JVM运行时两个类是否相等。

2、类加载的时机

类的加载可分为隐式加载和显示加载。

隐式加载

隐式加载包括以下几种情况:

  • 遇到new(new 一个实例对象的时候)、getstatic(获取一个类的静态字段的时候)、putstatic(设置一个类的静态字段的时候)、invokestatic(调用一个类的静态方法的时候)这4条字节码指令时。
  • 对类进行反射调用时。
  • 初始化一个类时,如果父类还没有初始化,则先加载其父类并初始化(但是初始化接口时,不要求先初始化父接口)
  • 虚拟机启动时,需要指定一个包含main函数的主类,优先加载并初始化这个主类。

显式加载

显示加载包含以下几种情况:

  • 通过Class.forName()加载
  • 通过ClassLoader的loaderClass方法加载
  • 通过ClassLoader的findClass方法

3、Class.forName()加载类

Class.forName()和ClassLoader都可以对类进行加载。ClassLoader就是遵循双亲委派模型最终调用启动类加载器的类加载器,实现的功能是“通过一个类的全限定名来获取描述此类的二进制字节流”,获取到二进制流后放到JVM中。Class.forName()方法实际上也是调用的CLassLoader来实现的。

先看看Class.forName()的源码:

/**
  * 参数解释:
  * 1、className:要加载的类名
  * 2、true:class被加载后是否要被初始化。初始化即执行static的代码(静态代码)
  * 3、caller:指定类加载器
  */
public static Class<?> forName(String className) throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

可以看出来最后正在实现forName()方法的是forName0这一个native方法。而使用forName0需要传递的参数之一,就是ClassLoader类加载器。因此可以知道Class.forName()本质还是使用classloader来进行类的加载的。

4、使用ClassLoader加载类

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()。

loadClass() 方法是加载目标类的入口,它首先会查找当前ClassLoader以及它的父类classloader里面是否已经加载了目标类,如果没有找到就会让父类Classloader尝试加载,如果父类classloader都加载不了,就会调用findClass()让自定义加载器自己来加载目标类。这实际上就是双亲委派机制的原理。

看一下loadClass()的源码:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //查看类是否已被加载,findLoadedClass最后调用native方法findLoadedClass0
            Class<?> c = findLoadedClass(name);
            //还未加载
            if (c == null) { 
                long t0 = System.nanoTime();
                try {
                    //若有父类加载器,则调用其loadClass(),请求父类进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        //若父类加载器为null,说明父类为BootstrapClassLoader(该类加载器无法被Java程序直接使用,用null代替即可),请求它来加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    //加载失败,抛出ClassNotFoundException异常
                }
                //父类无法加载,调用findClass方法,尝试自己加载这个类
                //注意:在这个findClass方法中,目前只是抛出一个异常,没有任何进行类加载的动作
                //因此,想要自己进行类加载,就要重写findClass()方法。
                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; 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;
        }
    }

ClassLoader 的 findClass() 方法是需要子类来覆盖重写的,不同的加载器将使用不同的逻辑来获取目标类的字节码。得到字节码之后会调用 defineClass() 方法将字节码转换成 Class 对象。

findClass()的源码如下:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name); //仅抛出异常
    }

可见,ClassLoader的findClass()方法是没有具体实现的,如果要自定义类加载器,就需要重写findClass()方法,并且配合defineClass() 方法一起使用,defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象。

以上就是CLassLoader进行类加载的简单流程。

虽然Class.forName()方法本质上还是使用Classloader来进行类的加载的,但它和使用Classloader来进行类加载依然有着区别:

①Class.forName()方法除了将类的字节码加载到jvm中之外,还会执行类中的static块,即会导致类的初始化。Class.forName(name, initialize, loader)带参方法也可以指定是否进行初始化,执行静态块。

②ClassLoader只是将类的字节码加载到jvm中,不会执行static中的内容,即不会进行类加载,只有在newInstance才会去执行static块。

5、自定义类加载器

我们知道,除了BootstrapClassLoader是由C/C++实现的,其他的类加载器都是ClassLoader的子类。所以如果我们想实现自定义的类加载器,首先要继承ClassLoader。

根据我们前面的分析,ClassLoader进行类加载的核心实现就在loadClass()方法中。再根据loadClass()方法的源码,我们可以知道有两种方式来实现自定义的类加载,分别如下:

①如果不想打破双亲委派机制,那么只需要重写findClass方法。
②如果想要打破双亲委派机制,那么就需要重写整个loadClass方法。

如果没有特殊要求,Java官方推荐重写findClass方法,而不是重写整个loadClass方法。这样既让我们能够按照自己的意愿加载类,也能保证自定义的类加载器符合双亲委派机制。

明确了如何实现,我们只需要两步就可以实现自定义的类加载器:第一步是继承classloader,第二步是重写findClass方法。

不过由于在findClass()内需要调用defineClass()方法将字节数组转换成Class类对象,因此要先对输入的class文件做一些处理,使其变为字节数组。

实现自定义的类加载器:

public class MyClassLoader extends ClassLoader{
    //默认ApplicationClassLoader为父类加载器
    public MyClassLoader(){
        super();
    }

    //加载类的路径
    private String path = "";

    //重写findClass,调用defineClass,将代表类的字节码数组转换为Class对象
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] dataByte = new byte[0];
        try {
            dataByte = ClassDataByByte(name);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return this.defineClass(name, dataByte, 0, dataByte.length);
    }

    //读取Class文件作为二进制流放入byte数组, findClass内部需要加载字节码文件的byte数组
    private byte[] ClassDataByByte(String name) throws IOException {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
        name = name.replace(".", "/"); // 为了定位class文件的位置,将包名的.替换为/
        is = new FileInputStream(new File(path + name + ".class"));
        int c = 0;
        while (-1 != (c = is.read())) { //读取class文件,并写入byte数组输出流
            arrayOutputStream.write(c);
        }
        data = arrayOutputStream.toByteArray(); //将输出流中的字节码转换为byte数组
        is.close();
        arrayOutputStream.close();
        return data;
    }
}

使用自定义的类加载器:

public static void main(String[] args) {
    MyClassLoader myClassLoader = new MyClassLoader();
    Class<?> clazz = myClassLoader.loadClass("com.fengjian.www.MyClassLoader");
    clazz.newInstance();
}

由于能力有限,可能存在错误,感谢指出。以上内容为本人在学习过程中所做的笔记。参考的书籍、文章或博客如下:
[1]Mr羽墨青衫.深入分析Java ClassLoader原理.知乎.https://zhuanlan.zhihu.com/p/81759029
[2]愚公要移山.学了这么久的java反射机制,你知道class.forName和classloader的区别吗?.知乎.https://zhuanlan.zhihu.com/p/101114197