简单地纯粹地记录下如何进行自定义一个自己的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
,很遗憾它是错误的。
写完后,就去测试跑了下,发现并没有出现我期许的结果 。
这里说下期许的结果是什么
- 加载
class
文件后生成的Class
对象,调用其getClassLoader
方法,应该是输出MyClassLoader
的 - 此
Class
对象和使用系统类加载器加载的同一个class
代表的Class
对象,并不相等,==
会返回false
- 自定义类加载器加载的对象,是没办法强转成系统类加载器加载的
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区都有一个自己的一亩三分地,这样在分配内存时,不需要加锁做同步,提升分配的效率。