摘自《Java高并发编程详解-多线程架构与设计》第九章 p158-p176
内置类加载器 bootstrap ClassLoader,Ext ClassLoader ,App ClassLoader。分别加载jre\lib,jre\lib\ext,-cp或者-classpath对应的classpath
通过继承ClassLoader重写findClass特殊目录来实现自定义类加载器。特殊目录或者设置父加载器为空,让loadClass时跳过父类加载器(ps,如父类已经加载同名类,父加载器非空则会返回cache)。重写loadClass可以完全绕过双亲委托。
class的实例是被类加载器的【实例】隔离。(当然不同的类加载器类型也会隔离) 因此代码中尽量得到同一个classLoader的实例,避免拿不到缓存,反复 findClass+defineClass。可以设置线程上下文类加载器。
在不指定parent的情况下,自定义ClassLoader的parent AppClassLoader是指的应用中唯一的AppClassLoader。因此加载classpath下已被加载过的类时,会因调用ClassLoader#getSystemClassLoader()对应AppClassLoader实例去调用findLoadedClass
去获取Class缓存
注意URLClassLoader#newInstance会调用到父类的ClassLoader()方法,导致传入系统的类加载器作为parent。而new URLClassLoader(url,null),会使得无parent类加载器。
ps:loadClass来观察,而不是使用Class.forName来观察。Class.forName在loadClass之前应该还有一次获取缓存的机会。
C++编写
-Xbootclasspath指定根加载器的路径。
sun.boot.class.path获得跟加载器加载的资源
jie\lib
Java编写,URLClassLoader的子类
用于加载 JAVA_HOME下的jre\lib\ext 里面的类库
java.ext.dirs可以获得扩展类加载器加载的类
jre\lib\ext
也可以将自己的类放到扩展类加载器的位置
负责加载 -cp/-classpath 指定的类库资源
自定义的类加载器都是ClassLoader的直接或间接在子类
需要重写抽象ClassLoader的 findClass方法。
需要指定一个父类类加载器。若不指定则绕过了双亲委派
需要自定义一个路径加载特殊的class,该目录不能为已经有类加载器使用过的目录。
可以使用loadClass打破双亲委派后,再使用任意路径。(相同的目录导致被委托给了父类类加载器加载)
得到类的二进制数据(无论网络/本地读取或动态代理/cglib生存),使用defineClass将其变成Class
/**
* @auth thewindkee
* @date 2018/12/15 0015 21:58
*/
public class MyClassLoader extends ClassLoader {
//public static final String LIB_PATH = "C:\\Users\\gkwind\\Desktop";
// 默认加载的位置
public static final String LIB_PATH = "E:\\gkrep\\gitee\\test\\other\\src\\main\\java\\com\\gkwind\\ClassLoader\\demo\\";
/*
* !不要定义在bootstrap,ext,app类加载器能加载的位置。
* 因为双亲委派的关系,会导致该类加载器失效。
* 如下定义必须修改loadClass打破双亲委派
*/
//public static final String LIB_PATH = MyClassLoader.class.getResource("/").getPath();
public MyClassLoader() {
//设置为null可以绕过双亲委派
//super(null);
}
public MyClassLoader(ClassLoader parent) {
super(parent);
}
protected Class> findClass(String name) throws ClassNotFoundException {
try {
//这里查找多层
byte[] bytes = getClassBytes(name);
//defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
Class> c = this.defineClass(name, bytes, 0, bytes.length);
System.out.println(c);
return c;
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(String name) throws Exception {
// 这里要读入.class的字节,因此要使用字节流
String resolvedPath = name.replace(".", "/");
File file = new File(LIB_PATH + resolvedPath + ".class");
System.out.println(MyClassLoader.class.getName() + "findClass:" + file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//FileInputStream fis = new FileInputStream(file);
//FileChannel fc = fis.getChannel();
//WritableByteChannel wbc = Channels.newChannel(baos);
//ByteBuffer by = ByteBuffer.allocate(1024);
//while (true) {
// int i = fc.read(by);
// if (i == 0 || i == -1)
// break;
// by.flip();
// wbc.write(by);
// by.clear();
//}
//fis.close();
//wbc.close();
java.nio.file.Files.copy(file.toPath(), baos);
return baos.toByteArray();
}
}
自定义的类加载器指定特殊目录或者指定父类加载器为空,避免双亲委派导致该类加载器失效
选择父类ClassLoader未加载的目录
为保证基础类不被破坏。加载类的时候优先祖师爷(递归父类)过目(加载)。 ----by 极客时间 jvm
loadClass 规定了双亲委托,所以可以直接重写loadClass打破双亲委托。
该方法是同步方法 – synchronized (getClassLoadingLock(name))
优先返回已经加载过的同名class --findLoadedClass(name)
loadClass(name,false)
false 指的是不做【连接(linked)阶段】的操作。这就是为什么加载类,导致类的初始化(【准备阶段】)。
不需要删除class.
1,2都是需要父类未加载过同名类,否则返回cache。3可以重复加载同名类
绕过系统类加载器,直接使用ext类加载器作为父类,并传入特殊的目录的class
无cache且父类加载不到特殊目录,自动到子类去加载。
设置父类类加载器为null
无cache且没有父类,自动到子类去加载
类加载器作为一个命名空间,可以隔离class
使用不同类加载器/或者同一加载器的不同实例去加载同一个类,会产生多个实例。
简单来说,class实例被不同的classLoader实例隔离。
classloader名+全路径列明
一个类的初始类加载器,包含尝试过加载的所有父类
此处使用了不同的类加载器 去隔离。
Demo.java
不同的位置的Demo.java输出的内容不同
MyClassLoader.java
注意加载class的位置不同!
MyClassLoader2.java
测试类
两个com.gkwind.Demo都被加载成功
MyClassLoader 继承ClassLoader
已知
1.Demo.class是自己编译的Class,不存在于
lib,lib/ext,classpath.在目录X下
2.A类对应A.class编译在classpath下。
3.MyClassLoader继承了ClassLoader,自定义了 findClass的目录X(App,Ext类加载器无法加载X目录),没有重写loadClass,更没有打破双亲委派。
问题:为何MyClassLoader加载Demo的时候,无法通过findLoadedClass获取Class的Cache?
加载A类,注意看两次new MyClassLoader的parent都是同一个实例。
演示时,造成A类与Demo类不一样的原因是:classpath对应的AppClassLoader 全局唯一。
new MyClassLoader().loadClass(“A”)多次,
委托parent->唯一的AppClassLoader去加载,第一次findClass,第二次findLoadedClass
new MyClassLoader().loadClass(“Demo”)多次,由于Demo.class不存在classpath对应的目录,导致由new MyClassLoader()实例去加载,每次都是新的ClassLoader实例,因此无法从findLoadedClass中获得缓存的Demo.class
结论: