ClassLoader是什么?
ClassLoader叫做类加载器,具体作用是在类加载阶段将.class文件加载到jvm虚拟机中,然后转成内存形式的Class对象供程序使用。还有就是jvm在运行时并不会立即加载所有的.class文件,而是按需加载的,当执行到未加载过的类时,才对该类进行加载,不然一次性加载,估计内存都炸了。
ClassLoader之间的关系
jvm在运行中会存在很多个ClassLoader,不同的ClassLoader用于加载不同路径下的类,有些用于加载本地文件中的class文件,有些用于加载jar包中的class文件等。
java默认提供了三个内置的ClassLoader,分别是BootStrapClassLoader
、ExtClassLoader
和AppClassLoader
:
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都持有一个parent属性,指向其父类加载器,当一个ClassLoader实例需要加载某个未知的类时,它会先判断这个类是否已加载过了,如果未加载过,则把这个任务交给它的父类加载器,由它的父类加载器去检查,一直委托到BootStrapClassLoader,如果BootStrapClassLoader也没有加载过这个类,那么它就会从它指定的路径中去查找,如果查找成功则返回,否则交由子类加载器(这里是ExtClassLoader)进行查找,直到委托的发起者,由它在指定的路径下去加载该类,如果还是没有加载到,则抛出ClassNotFoundException异常。
整个流程如下图所示:
为什么要使用双亲委托机制?
因为这样可以避免重复加载同一个类,保证类的唯一性,当父类加载器已经加载过该类时,则子类加载器就没有必要再加载一次。假设不使用委托机制,那我们可以自定义一个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