自定义ClassLoader

Posted heartlake

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了自定义ClassLoader相关的知识,希望对你有一定的参考价值。

简单地纯粹地记录下如何进行自定义一个自己的ClassLoader

什么双亲委派模型啊,双亲委派模型的破坏啊,好处啊,缺点啊什么的,一概不说。

自定义ClassLoader的博客啥的,看过不少,但是就是没自己亲手写一下,今天尝试写一下,发现古人诚不欺我!

纸上得来终觉浅,绝知此事要躬行

失败版本

最开始是这么写的

public class MyClassLoader extends ClassLoader {

    @Override
    protected Class findClass (String name) throws ClassNotFoundException {

        String classPath = name.replace(".", "/");
        InputStream classInputStream = getSystemClassLoader().getResourceAsStream(classPath);
        try {
            byte[] classBytes = new byte[classInputStream.available()];
            classInputStream.read(classBytes);
            Class clazz = defineClass(name, classBytes, 0, classBytes.length);
            resolveClass(clazz);
            return clazz;
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

    }
}

这里错误比较多,不过还是记住了一个,我是重写了findClass方法,而不是重写了loadClass方法,推荐也是通过重写findClass方法,以前是重写loadClass方法的方式。

即使是错误的,但是写之前还是绞尽脑汁的想了好久,试图把记忆中那点破碎的,分崩离析而又即将消失的关于自定义ClassLoader的记忆,给重新恢复了。可惜的是,我并不具体这个能力,凭着那点仅存的记忆,写下我的第一个自定义ClassLoader,很遗憾它是错误的。

写完后,就去测试跑了下,发现并没有出现我期许的结果 。

这里说下期许的结果是什么

  1. 加载class文件后生成的Class对象,调用其getClassLoader方法,应该是输出MyClassLoader
  2. Class对象和使用系统类加载器加载的同一个class代表的Class对象,并不相等,==会返回false
  3. 自定义类加载器加载的对象,是没办法强转成系统类加载器加载的Class类型。

然后,没有一个结果符合预期的。

看到输出的ClassLoader还是AppClassLoader,很奇怪,我明明自定义了类加载还去加载了啊!

最终发现,直接继承ClassLoader时,使用默认的无参构造

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

默认情况下,继承自ClassLoader的子类,会拥有一个父类加载,就是 AppClassLoader,而 要加载的类 ,发现已经被父类加载器加载过了,所以实际上并没有子类的findClass方法

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 {
                  	// 不存在父加载器,使用启动类加载器去加载 
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
	    // 如果以上都找不到,就使用下面的逻辑去查找 
            if (c == null) {
                long t1 = System.nanoTime();
                // 这个就是各个子类来实现的了
                c = findClass(name);

                // 一些信息记录,记录到虚拟机里
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
      	// 上面仅仅完成了一个加载class的动作,但是整个类的加载并没有完成
        // 如果需要解析,则会对Class对象进行解析,这个名字有误导性,其实这是类加载阶段的链接阶段 
        // 也就是 验证 准备 解析三个阶段
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

所以问题就很明了了,

第一次修改后的版本

public class MyClassLoader extends ClassLoader {
    public MyClassLoader () {
      // 不使用系统类加载器作为此类加载的父加载器
      // 这样它的父加载器就是启动类加载器
      super(null);
    }
    @Override
    protected Class findClass (String name) throws ClassNotFoundException {

        String classPath = name.replace(".", "/");
        InputStream classInputStream = getSystemClassLoader().getResourceAsStream(classPath);
        try {
            byte[] classBytes = new byte[classInputStream.available()];
            classInputStream.read(classBytes);
            Class clazz = defineClass(name, classBytes, 0, classBytes.length);
            resolveClass(clazz);
            return clazz;
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

    }
}

这个一跑,也是完蛋,不过好解决。

一般调用loadClass方法时,传的都是包名,这里是要去加载字节码的,也就是找class文件,所以要转换成具体的路径,这里的路径使用的是相对路径,类位于classpath目录下,所以直接使用ClassLoader#getResourceAsStream就可以获取class文件的字节流`了。

这里实现的字节码来源是从文件系统加载的class文件,实际上任何符合Java虚拟机规范的Class结构的字节数组,都可以被加载进来,动态代理就是在运行时生成字节码,然后直接加载的。

可运行版本

public class MyClassLoader extends ClassLoader {

    public MyClassLoader () {
        super(null);

    }

    @Override
    protected Class findClass (String name) throws ClassNotFoundException {

        String classPath = name.replace(".", "/")+".class";
        InputStream classInputStream = getSystemClassLoader().getResourceAsStream(classPath);
        try {
            byte[] classBytes = new byte[classInputStream.available()];
            classInputStream.read(classBytes);
            Class clazz = defineClass(name, classBytes, 0, classBytes.length);
            resolveClass(clazz);
            return clazz;
        } catch (IOException e) {
            throw new ClassNotFoundException();
        }

    }
}

这就是一个麻雀虽小五脏俱全的自定义类加载器了。

两个重要知识点

就想到这俩,肯定不止俩

同一个类的Class对象在同一个虚拟机进程中,可以存在多个实例,在虚拟机中,是根据Class所属的类加载器,来确定唯一一个Class

Hotspot虚拟机在进行类加载时,采用了类似的TLAB的方式,会给每个类加载器分配一块内存,这样这个类加载器加载的类,直接在这里分配,提高效率,也便于管理,不过遇到有很多类加载的话,会出现OOM的可能,原因就是每个类加载器分配一块,多整一些 ,空间不够了,OOM

TLAB(Thread Local Allocate Buffer),目的是提升性能的,每一个线程在新生代的Eden区都有一个自己的一亩三分地,这样在分配内存时,不需要加锁做同步,提升分配的效率。

以上是关于自定义ClassLoader的主要内容,如果未能解决你的问题,请参考以下文章

自定义类加载器classLoader

自定义ClassLoader

自定义ClassLoader

VSCode自定义代码片段——CSS选择器

classloader加载class的流程及自定义ClassLoader

VSCode自定义代码片段6——CSS选择器