理解ClassLoader

ClassLoader是什么?

ClassLoader叫做类加载器,具体作用是在类加载阶段将.class文件加载到jvm虚拟机中,然后转成内存形式的Class对象供程序使用。还有就是jvm在运行时并不会立即加载所有的.class文件,而是按需加载的,当执行到未加载过的类时,才对该类进行加载,不然一次性加载,估计内存都炸了。

ClassLoader之间的关系

jvm在运行中会存在很多个ClassLoader,不同的ClassLoader用于加载不同路径下的类,有些用于加载本地文件中的class文件,有些用于加载jar包中的class文件等。
java默认提供了三个内置的ClassLoader,分别是BootStrapClassLoaderExtClassLoaderAppClassLoader

BootStrapClassLoader

启动类加载器,是Java类加载层次中最顶层的类加载器,用于加载核心类库,如:lib文件下的resources.jar、rt.jar、charsets.jar等,称为根加载器。

ExtClassLoader

扩展类加载器,负责加载JAVA_HOME/jre/lib/ext/目录下的所有jar包。

AppClassLoader

系统类加载器,负责加载应用程序classpath目录下所有的jar包和class文件。我们编写的类和使用的第三方jar包都是由它来加载的。

除了以上三个ClassLoader以外,用户还可以根据自己的需要自定义类加载器,而这些自定义的ClassLoader必须要继承自java.lang.ClassLoader类(AppClassLoader和ExtClassLoader也都是继承自ClassLoader类),但需要注意的是BootStrapClassLoader并不是继承自ClassLoader的,因为它不是个普通的Java类,它是由C/C++编写的,本身是虚拟机的一部分,JVM启动时通过BootstrapClassLoader加载rt.jar等核心类库后,再创建和初始化ExtClassLoader和AppClassLoader类加载器。

除了根加载器(BootStrapClassLoader)没有父类加载器外,其他的类加载器都有一个父类加载器,扩展类加载器(ExtClassLoader)的父类加载器就是根加载器,系统类加载器(AppClassLoader)的父类加载器就是扩展类加载器,对于自定义的类加载器来说,它的父类加载器一般是系统类加载器。

每个ClassLoader对象都有一个parent属性指向它的父类加载器:

public abstract class ClassLoader {
  	...
    // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
	...
}

他们之间的关系可以用下图表示:
ClassLoader继承关系

ClassLoader加载原理

当程序在运行中遇到一个未知的类的时候,会使用这个类的调用者的ClassLoader来加载它,而ClassLoader使用的是双亲委托机制来搜索类的,每个ClassLoader都持有一个parent属性,指向其父类加载器,当一个ClassLoader实例需要加载某个未知的类时,它会先判断这个类是否已加载过了,如果未加载过,则把这个任务交给它的父类加载器,由它的父类加载器去检查,一直委托到BootStrapClassLoader,如果BootStrapClassLoader也没有加载过这个类,那么它就会从它指定的路径中去查找,如果查找成功则返回,否则交由子类加载器(这里是ExtClassLoader)进行查找,直到委托的发起者,由它在指定的路径下去加载该类,如果还是没有加载到,则抛出ClassNotFoundException异常。

整个流程如下图所示:
ClassLoader双亲委托机制

为什么要使用双亲委托机制?

因为这样可以避免重复加载同一个类,保证类的唯一性,当父类加载器已经加载过该类时,则子类加载器就没有必要再加载一次。假设不使用委托机制,那我们可以自定义一个String类替换掉系统的String类,这明显会出问题的,而使用了双亲委托机制,因为在启动时String类就被启动类加载器(BootstrapClassLoader)加载,所以我们自定义的ClassLoader无法加载到我们自定义的String类。

自定义类加载器

由于Java提供的默认ClassLoader只加载指定路径下的jar包和class文件,如果我们想要加载其它路径下的类或jar包时,比如,从网络上下载的class文件等,那么默认的ClassLoader就不能满足我们的需求了,这时候我们就需要自定义一个ClassLoader。

ClassLoader有三个比较重要的方法:

1、loadClass()

默认实现:

public Class<?> loadClass(String name) throws ClassNotFoundException {
       return loadClass(name, false);
}

再看下loadClass(String name, boolean resolve)函数:

protected Class<?> loadClass(String name, boolean resolve)
       throws ClassNotFoundException
   {
           // First, check if the class has already been loaded
           Class<?> c = findLoadedClass(name);
           if (c == null) {
               try {
                   if (parent != null) {
                       c = parent.loadClass(name, false);
                   } else {
                       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.
                   c = findClass(name);
               }
           }
           return c;
   }

整理一下该函数大致流程:
1、执行findLoadedClass去检测指定名称的类是否已经加载过了,如果已经加载过,则直接返回。
2、如果该类没有加载过,则调用其父类加载器的loadClass方法,如果父类加载器为空,则使用BoostrapClassLoader去加载该类。
3、如果向上委托都没有找到该类,则调用当前类加载器的findClass去加载类。

2、findClass()

默认实现:

protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
}

可以看出,该方法默认是抛出异常的。而在loadClass方法中可以看到,在父类加载器无法加载类的时候,就会调用我们自定义的类加载器中的findeClass函数,从而完成类的加载,如果该类未找到,则会抛出异常。

3、defineClass()

默认实现:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError
{
    throw new UnsupportedOperationException("can't load this type of class file");
}

该方法的作用是将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。如,假设class文件是加密过的,则需要解密后作为形参传入defineClass函数。

注意:自定义类加载器不应该破坏双亲委托机制,避免重写loadClass方法,否则可能会导致无法加载一些核心类库。在使用自定义类加载器时,如果不指定它的父类加载器,则默认是AppClassLoader,如果父类加载器是 null,那就表示父加载器根加载器。

自定义ClassLoader示例

首先我们先编写一个测试的Java类(Test.java):

public class Test {

    public void say(){
        System.out.println("I am JW");
    }

}

然后将它编译生成的.class文件放到D://classloader文件夹下,然后编写个自定义的类加载器去加载这个目录下的.class文件:

class MyClassLoader(private val classFilePath: String) : ClassLoader() {

    override fun findClass(name: String?): Class<*> {

        name?.apply {
            val fileName = getFileName(this) // 获取文件名
            val fileToByte = fileToByte(fileName) // 将文件转成字节数组
            
            // 传入defineClass函数,生成Class对象后返回
            return defineClass(name, fileToByte, 0, fileToByte.size)
        }

        return super.findClass(name)
    }

    /**
     * 将文件转成字节数组
     */
    private fun fileToByte(fileName: String): ByteArray {
        val file = File(classFilePath, fileName)

        val fis = FileInputStream(file)
        val bos = ByteArrayOutputStream()

        var len: Int = -1
        fis.use { input ->
            bos.use { output ->
                while (input.read().also { len = it } != -1) {
                    output.write(len)
                }
            }
        }

        return bos.toByteArray()!!
    }

    /**
     * 获取文件名,后缀加上.class
     */
    private fun getFileName(name: String): String {
        val lastIndex = name.lastIndexOf(".")
        return if (lastIndex != -1) {
            "${name.substring(lastIndex + 1)}.class"
        } else {
            "$name.class"
        }
    }

}

接下来测试下我们刚编写的ClassLoader效果怎样:

class Main {

    companion object {
        @JvmStatic
        fun main(args: Array<String>) {
            /*  println("extClassLoader path: ${System.getProperty("java.ext.dirs")}")
              println("security path: ${System.getProperty("java.security.manager")}")
              println("bootstrapClassLoader path: ${System.getProperty("sun.boot.class.path")}")
              println("appClassLoader path: ${System.getProperty("java.class.path")}")*/

            try {
                val myClassLoader = MyClassLoader("D://classloader")
                val loadClass = myClassLoader.loadClass("com.jw.newapplication.Test")

                if (loadClass != null) {
                    val instance = loadClass.newInstance()
                    val method = loadClass.getDeclaredMethod("say")

                    // 通过反射调用方法
                    method.invoke(instance)
                }
            } catch (e: Throwable) {
                e.printStackTrace()
            }

        }
    }

}

运行并查看结果:
在这里插入图片描述
到这里,我们编写的ClassLoader就测试成功了,可以正常加载指定路径下的.class文件。

总结

ClassLoader主要是用来加载指定路径下的jar包或.class文件的,由于有双亲委托机制,可以避免重复加载一个类。当自定义类加载器时,一般只需要重写findClass,实现查找逻辑就可以了,不建议重写loadClass方法。

参考文章:
http://www.importnew.com/24036.html
http://blog.itpub.net/31561269/viewspace-2222522/
https://blog.csdn.net/briblue/article/details/54973413

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值