Java类加载机制-类加载器(ClassLoader)与双亲委派模型
自建博客地址:https://www.bytelife.net,欢迎访问! 本文为博客自动同步文章,为了更好的阅读体验,建议您移步至我的博客👇
本文作者: Jeffrey
本文链接: https://www.bytelife.net/articles/44598.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
Java虚拟机类加载过程中的“加载”阶段第一步就是“通过一个类的全限定名来获取描述此类的二级制字节流”,这个动作由Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的模块叫做“类加载器”。
类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中的作用远不限于此。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。
换言之,比较两个类“相等”,只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
Java中的类加载器
Java虚拟机只有两种不同的类加载器:
- 启动类加载器(Bootstrap ClassLoader):使用C++语言(HotSpot)实现,是虚拟机的一部分,该类加载器实例无法被用户获取;
- 所有其它的类加载器:均由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader;
从Java程序员的角度,类加载器还可以继续细化,绝大部分Java程序都会使用到以下3种类加载器。
- 启动类加载器 (Bootstrap ClassLoader):这个类加载器负责将存放在
<JAVA_HOME>\lib
目录中的,或者被-Xbootclasspath
参数所指定的目录中的,并且是虚拟机识别的(仅按照文件名识别,例如rt.jar)类库加载到虚拟机内存中。 启动类加载器无法被Java程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委托给引导类加载器,直接使用null代替即可。 - 扩展类加载器(Extension ClassLoader):这个加载器由
sun.misc.Launcher$ExtClassLoader
实现,他负责加载<JAVA_HOME>\lib\ext
目录中的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。 - 应用程序类加载器(Application ClassLoader):这个类加载器由
sun.misc.Launcher$AppClassLoader
实现。该类是ClassLoader中的getSystemClassLoader()
方法的返回值,因此也称作“系统类加载器”。它负责用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序的默认类加载器。
应用程序一般由这3中类加载器相互配合加载,如果有必要,还可以加入自己定义的类加载器,集成。
自定义类加载器
自定义类加载器可以直接或间接继承自类java.lang.ClassLoader
。在java.lang.ClassLoader
类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)
方法即可。 java.lang.ClassLoader类的方法 loadClass()封装了代理模式的实现。
- 该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;
- 如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;
- 如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。
因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。 下面是一个文件系统类加载器的例子:
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
双亲委派模型
下图展示的类加载器之间的层次关系,称为类加载器的“双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其它类加载器必须有自己的父加载器。
这里的类加载器之间的父子关系一般不通过继承(Inheritance)来实现,而是通过组合(Composition)关系来服用父加载器代码。 双亲委派模型并不是一个强制性约束,而是Java设计者推荐给开发者的一种类加载实现方式。
双亲委派模型的工作过程
- 如果一个类加载器收到了类加载的请求,它不会先自己尝试处理这个请求,而是委派给它的父类加载器,所有的请求最终都会传送到顶层的启动类加载器
- 只有当父类反馈自己无法完成该请求(它的搜索范围中没有找到所需的类,即抛出ClassNotFoundException)时,子加载器才会尝试自己加载。
为什么使用双亲委派模型?
使用双亲委派模型可以使得Java类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类java.lang.Object
,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器加载,因此Objcet类在程序中的各种类加载器环境中都是同一个类。 如果没有使用双亲委派模型,那么如果用户自己写了一个称为“java.lang.Object”
的类,并放在程序的classpath中,那么系统将产生多个不同的Object类,可想而知,程序将一片混乱。
双亲委派模型的实现
双亲委派模型的实现非常简单,几乎所有的代码仅在loadClass()
方法中实现,下面是一个简单的例子:
//双亲委派模型的实现源码
protected synchronized Class<?> loadClass(String name, Boolean resolve) throws ClassNotFoundException{
// 1、首先检查请求的类是否已经被加载过
Class c = findLoadedClass(name);
if(c == null){
try{
if(parent != null){ // 2、如果没有则调用父加载器的loadClass()方法
c = parent.loadClass(name, false);
// 3、如果父加载器为空则默认使用启动类加载器作为父加载器
} else{
c = findBootstrapClassOrNull(name);
}
}catch(ClassNotFoundException e){
// 4、如果父类加载器加载失败,则先抛出ClassNotFoundException
}
// 5、然后再调用自己的findClass()方法进行加载
if(c == null){
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}