目录
概述
唯一性
类加载器
演示一
演示二
双亲委派模型概述
演示
双亲委派实现源码
AppClassLoader分析
破坏双亲委派
自定义类加载器
概述
通过一个类的全限定名来获取该类的二进制字节流就是类加载器(Class Loader)要干的事。前一篇我们讲解了类加载机制,这一篇和类加载机制中的加载阶段相呼应,这个动作放在了虚拟机外部去实现的,所以我们能够自己实现自定义的类加载器。
唯一性
在java中你用某个类加载器加载了一个类,那么就必须保证这个类加载器和这个类组成一个唯一的组合。每个类加载器都有一个独立的类名称空间,所以两个同名的类是不同的类加载器加载出来的就不算相同,所以判断一个类是否相同,前提是他们的类加载器也相同。
类加载器
以jdk8为例
类加载器分两张不同,一种是启动类加载器Bootstrap ClassLoader,c++实现的,是虚拟机自身的一部分,第二种是其他所有类加载器,java语言实现,独立于虚拟机之外,并且全部继承自java.lang.ClassLoader抽象类。
- 启动类加载器(Bootstrap ClassLoader):负责加载
/jre/lib目录下或者被-Xbootclasspath参数所指定的路径,如rt.jar、tools.jar都是他加载的,用于加载java的核心类库。 - 扩展类加载器(Extension ClassLoader):用于加载
/jre/lib/ext路径下的类或者被java.ext.dirs系统变量所指定的路径。 - 应用程序类加载器(Application ClassLoader):用于加载用户类classpath路径下的所有类库,是ClassLoader.getSystemClassLoader()的返回值,所以也叫做系统类加载器,如果我们编写的代码没有使用自定义的类加载器,默认都是应用程序类加载器加载的。
演示一
我们自定义一个类MyClass
public class MyClass {
static {
System.out.println("MyClass init...");
}
}
加载MyClass类进方法区,并查看是哪个类加载器加载的它
public class Test {
public static void main(String[] args) throws ClassNotFoundException {
//通过全限定名反射加载和初始化该类
Class> klass = Class.forName("classloader.MyClass");
//获得该类的类加载器
System.out.println(klass.getClassLoader());
}
}
结果为
MyClass init...
sun.misc.Launcher$AppClassLoader@18b4aac2
是通过应用程序类加载器加载的。
演示二
我们加上虚拟机的参数,试图让启动类加载器加载它
注意需要加上/a使用追加的方式,否则会覆盖原来的值,启动类加载器连Object都不会加载了。
冒号后面的点代表当前目录追加上去,注意不要使用IDE工具,因为IDE工具的.不是原生class路径。
结果是null,代表是启动类加载器去加载的我们的类,因为c++的程序我们不能够直接访问。
双亲委派模型概述
双亲委派模型
上面展示的层次关系就是双亲委派模型,双亲委派模型要求除了顶层的启动类加载器之外,其他的类加载器都需要有一个父类加载器,图中层次关系已经很明显了,启动类加载器的父类可理解为null。
双亲委派中的父子关系一般是用组合的关系来实现的,而不是继承关系
双亲委派模型的工作过程是:如果一个类加载器收到了一个类加载的请求,他首先不会去自己去尝试加载这个类,而是把该任务委派给父加载器去完成,会一直往上送,一直到顶层的启动类加载器中,当父类加载器无法完成这个任务时,自加载器才会尝试自己去加载。
- 优点
为什么要这样做呢?其实这种模式有一个很大的好处,就是保证了程序中类的唯一性。使得java类会随着它的类加载器组成一个带有优先关系的层次关系,例如java.lang.Object类,他存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都会被顶层的启动类加载器加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。
反之如果不遵循双亲委派模型,都有各个类加载器自己去加载的话,如果我现在也编写一个类叫java.lang.Object放在我的classpath下,那程序中就会出现多个不同的Object类,这就导致java程序中一片混乱,后果可想而知。
演示
我在我的classpath下面写了一个类,把这类打包成jar包,然后放到扩展类的目录下,原来的类不删除,保存他们两同名,看看加载情况。
①打包为my.jar
②剪切到ext目录下
③修改一下打印结果,方便辨认。
④执行程序
⑤结果为
加载的类是我们ext路径下的jar包中的类,且是由扩展类加载器加载的。
结论:我们想加载一个类首先通过应用程序类加载器,它往上先给到它的父类扩展类加载器,扩展类加载器再给到父类启动类加载器,启动类加载器发现指定的路径没有这个名字的类,所以启动类加载不了,还给他的儿子扩展类加载器,扩展类发现了ext路径下有这个名字的类,所以它能加载,就直接加载了这个类,不会等到它的来子类加载了。
双亲委派实现源码
我们找到java.lang.ClassLoader抽象类的loadClass()方法,这就是双亲委派的源码。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先检查该类是否已经加载过
Class> c = findLoadedClass(name);
if (c == null) {
try {//如果父类不为空就找父类的loadClass()方法
if (parent != null) {
//parent是组合在本类中的
c = parent.loadClass(name, false);
} else {//如果为空则当前为Bootstrap类加载器
c = findBootstrapClassOrNull(name);//该方法底层是native修饰的
}
} catch (ClassNotFoundException e) {
//如果父类加载不了会抛出ClassNotFoundException异常
}
//如果c还是为空则代表父类没有加载
if (c == null) {
// 此时再调用本身的findClass()继续进行类加载
//findClass是每个类加载器需要实现的,用于在指定的位置查找类并加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
AppClassLoader分析
如图,URLClassLoader是AppClassLoader和ExtClassLoader的父类,并继承了ClassLoader
找到该URLClassLoader继承ClassLoader实现的findClass方法,看看他是如何找到类并加载的
其中findClass调用了defineClass方法,defineClass方法做的是将class二进制内容转换为Class对象,不符合要求的抛出异常。
我们不需要管defineClass,只需知道我们找到class文件后,把该class文件读到一个byte数组里,然后调用defineClass(String name, byte[] b, int off, int len)即可,defineClass他会给我们把字节数组转为类加载到方法区里。
破坏双亲委派
双亲委派缺点:
带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类。应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
如JNDI服务,它的代码由启动类加载完成(rt.jar),JNDI存在的目的就是查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的Classpath下的JNDI服务提供者接口(SPI,Service Provider Interface)的代码,现在问题来了,启动类加载器是绝不可能认识、加载这些代码的,那该怎么办?
为了解决这个困境,Java的设计团队只好引入了一个不太优雅的设计:线程上下文类加载(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
有了线程上下文类加载器,程序就可以做一些“舞弊”的事情了。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情。Java中涉及SPI的加载基本上都采用这种方式来完成,例如JNDI、JDBC、JCE、JAXB和JBI等。不过,当SPI的服务提供者多于一个的时候,代码就只能根据具体提供者的类型来硬编码判断,为了消除这种极不优雅的实现方式,在JDK 6时,JDK提供了java.util.ServiceLoader类,以meta-INF/services中的配置信息,辅以责任链模式,这才算是给SPI的加载提供了一种相对合理的解决方案。
默认上下文加载器就是应用类加载器,这样以上下文加载器为中介,使得启动类加载器中的代码也可以访问应用类加载器中的类。
线程上下文类加载器是当前线程使用的类加载器,默认就是应用程序类加载器,它内部又是由Class.forName调用了线程上下文类加载器完成类加载,具体代码在ServiceLoader的内部类LazyIterator中
破话双亲委派源码分析:打破双亲委派之SPI、线程上下文类加载器、ServiceLoader_清风-CSDN博客
自定义类加载器
什么时候我们需要自定义类加载器呢?
- 想加载不在classpath目录中的类文件
- 都是通过接口来使用实现,希望解耦时,常用在框架设计
- 这些类希望予以隔离,不同应用的同名类都可以加载,不冲突,常见于tomcat容器
步骤:
- 继承ClassLoader
- 要遵从双亲委派机制,重写findClass方法,注意不是重写loadClass方法,否则不会走双亲委派机制
- 读取类文件的字节码
- 调用父类的defineClass方法来加载类
- 使用者调用该类加载器的loadClass方法
如下我们写一个加载磁盘上指定路径的class的类加载器:
public class MyClassLoader extends ClassLoader {
private String path;//我们要加载的类的路径
public MyClassLoader(String path) {
this.path = path;
}
@Override //类名称
protected Class> findClass(String name) throws ClassNotFoundException {
String filename = getFileName(name);
File file = new File(path, filename);
try {
//获取该class文件流,读取它
FileInputStream fis = new FileInputStream(file);
//临时存放在ByteArrayOutputStream中,方便转为byte[]
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int len = 0;
try {
while ((len = fis.read()) != -1) {
bos.write(len);
}
} catch (IOException e) {
e.printStackTrace();
}
//转为字节数组的形式
byte[] data = bos.toByteArray();
fis.close();
bos.close();
//调用defineClass加载类,并返回
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
e.printStackTrace();
}
//抛异常
return super.findClass(name);
}
//java.lang.Object=>Object.class
private String getFileName(String name) {
int index = name.lastIndexOf(".");
if (index == -1) {
return name + ".class";
} else {
return name.substring(index + 1) + ".class";
}
}
}
演示
public class T {
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader("D://demo");
try {
Class> c = classLoader.loadClass("classt.Test");
if(c!=null){
Object o = c.newInstance();
Method m=c.getDeclaredMethod("say",null);
m.invoke(o,null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//正确
注意我们这里是遵循双亲委派的,父加载器都加载不了我们磁盘路径上的class文件,只有退回到我们的自定义类加载器时才能加载成功。如果某些情况不想遵循或者想先用我们的自定义类加载器加载的话,可以重写loadClass()方法。且我们自定义类加载器的parent属性就是APPClassLoader,这一点不用担心,我们重写loadClass()方法时代码中可以随时让parent加载。



