简述 Java 类加载器和双亲委派模型
封面的 JAVA 是我用在线网站生成的,好看吧(大概
Java 类加载器有什么用
顾名思义,类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java
文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class
文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class
类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的newInstance()
方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的
基本上所有的类加载器都是java.lang.ClassLoader
类的一个实例,下面仔细介绍下它:java.lang.ClassLoader
类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即java.lang.Class
类的一个实例。除此之外,ClassLoader
还负责加载 Java 应用所需的资源,如图像文件和配置文件等,为了完成加载类的这个职责,ClassLoader
提供了一系列的方法,比较重要的方法如下所示:
方法 | 说明 |
---|---|
getParent() |
返回该类加载器的父类加载器 |
loadClass(String name) |
加载名称为name 的类,返回的结果是java.lang.Class 类的实例 |
findClass(String name) |
查找名称为name 的类,返回的结果是java.lang.Class 类的实例 |
findLoadedClass(String name) |
查找名称为name 的已经被加载过的类,返回的结果是java.lang.Class 类的实例 |
defineClass(String name, byte[] b, int off, int len) |
把字节数组b 中的内容转换成 Java 类,返回的结果是java.lang.Class 类的实例。这个方法被声明为final 的 |
resolveClass(Class<?> c) |
链接指定的 Java 类 |
几种类加载器
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
- 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自
java.lang.ClassLoader
- 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类
- 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过
ClassLoader.getSystemClassLoader()
来获取它(即 AppClassLoader)
除了系统提供的类加载器以外,开发人员可以通过继承java.lang.ClassLoader
类的方式实现自己的类加载器,以满足一些特殊的需求
除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过getParent()
方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构:
双亲委派模型
双亲委派加载模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类
双亲委派加载模型的好处:
- 通过委派的方式,可以避免类的重复加载,当父加载器已经加载过某一个类时,子加载器就不会再重新加载这个类
- 通过双亲委派的方式,还保证了安全性。因为 Bootstrap ClassLoader 在加载的时候,只会加载
JAVA_HOME
中的 jar 包里面的类,如java.lang.Integer
,那么这个类是不会被随意替换的,除非有人跑到你的机器上, 破坏你的 JDK
双亲委派模型并不是一个强制性约束,而是 Java 设计者推荐给开发者的类加载器的实现方式,在一定条件下,为了完成某些操作,可以破坏模型
尝试自定义类加载器
虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,还是需要为应用开发出自己的类加载器
这个类加载器用来加载存储在文件系统上的 Java 字节代码:
1 | import java.io.*; |
测试类的接口:
1 | public interface TestInterface { |
测试类:
1 | public class TestClass implements TestInterface { |
测试加载代码:
1 | public class Main { |
如果上面的代码打印的结果不是MyClassLoader@...
,那么这个类就没有按照我们预期的那样由我们自定义的类加载器加载,原因可能在于编译生成的TestClass.class
存在在 classpath,需要把它删除并复制一份到E:\Temp
下,这可能因为我们编写的类加载器仍然遵循双亲委派的模型,由我们编写的类加载器的父类加载器加载了TestClass
类,我们需要把它移出 classpath,这样父类加载器就无法加载这个类,就会让我们编写的类加载器来加载
如果不想打破双亲委派模型,那么只需要重写 findClass 方法即可,如果想打破双亲委派模型,那么就重写整个 loadClass 方法
一般来说,自己开发的类加载器只需要覆写
findClass(String name)
方法即可。java.lang.ClassLoader
类的方法loadClass()
封装了前面提到的双亲委派的实现。该方法会首先调用findLoadedClass()
方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的loadClass()
方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用findClass()
方法来查找该类。因此,为了保证类加载器都正确实现双亲委派,在开发自己的类加载器时,最好不要覆写loadClass()
方法,而是覆写findClass()
方法
除此之外,还可以自定义类加载器加载网络的类,这里就不再详细介绍了,另外一个类是由类本身和加载它的类加载器共同确定的,由不同类加载器加载的同一个类不相同