类加载机制的第一步就是“加载”,即将Class文件获取二进制字节流并加载到方法区中
这个“加载”动作是放在JVM 之外去实现的,能够让应用程序来决定如何获取所需要的类
类和类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同决定其在JVM中的唯一性
简答点说:比较两个类是否是同一个类,只有在这两个类是由同一个类加载器加载的前提下才有意义。
否则,即使这两个类来源于同一个Class文件,加载到同一个JVM中,但是它们的类加载器不是同一个,那么就认为这是两个不同的类。
这个”加载“动作是由类加载器完成的,而且”加载“动作放在了JVM之外,所以我们可以自定义类加载器来实现自己的应用程序
可以通过Class.getClassLoader()获取一个类的类加载器
双亲委派模型
站在JVM的角度来看,只存在两种不同的类加载器:
- 一种是启动类加载器,Bootstrap ClassLoader,这个类加载底层使用C++来实现,这种类加载器属于JVM自身。
- 另一种是其他的所有类加载器,这些类加载器是由Java语言实现,独立于JVM之外,并且全部都继承自抽象类java.lang.ClassLoader
站在Java开发人员的角度来看,类加载器会更加详细。
三层类加载器和双亲委派模型
绝大部分的Java应用程序都会通过以下3个系统提供的类加载器来进行加载:
-
启动类加载器
这个类加载器负责加载存放在<JAVA_HOME>\lib
目录或被XBootclasspath
指定的路径中的类,并且是JVM能够识别的类
前面说了,这个启动类加载器是JVM自己的,底层是由C++实现的,所以启动类加载器无法在Java应用程序中进行直接引用。 -
扩展类加载器
这个类加载负责加载存放于<JAVA_HOME>\lib\ext
目录中或被java.ext.dirs
系统变量所指定的路径中的类
我们可以将自定义的通用的类放在ext目录下来扩展JavaSE的功能
由于扩展类加载器是由Java实现的,所以我们可以在Java应用程序中直接使用这个扩展类加载器来加载Class文件 -
应用程序类加载器
这个应用程序类加载器ClassLoader类中getSystemClassLoader()的返回值
负责加载用户类路径classpath上的所有类,也被称为“系统类加载器”
如果应用程序中没有自定义类加载器,那么一般情况下这个应用程序类加载器就是程序中默认的类加载器**
JDK9之前的Java应用程序都是由这三种类加载器互相配合来完成的,我们还可以通过自定义类加载器来进行拓展
这些类加载器之间的协作关系是
这个图展示的各种类加载器之间的层次关系就被称为类加载器的“双亲委派模型”
双亲委派模型要求:除了顶层的启动类加载器之外,其余的所有类加载器都要有自己的父类类加载器
这里的类加载器之间的父子关系不是通过继承来实现的,通常是使用组合来复用父类类加载器的代码
双亲委派模型的的工作流程:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求 委派给 父类加载器去完成,每一个层次的类加载器都是这样
因此,所有的类加载请求最终都应该传送到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(他的搜索范围中没有找到所需要的这个类),子加载器才会尝试自己去完成加载
这样做的好处是:Java中的类随着它的类加载一起具备了带有优先级的层次关系
简单点说,因为类加载器有优先级,所以由不同类加载器加载的类也就具有了优先级。
例如:java.lang.Object,他存放与rt.jar中,无论是哪一个类加载器要加载这个类,最终都会委派给最顶层的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。反之,如果没有双亲委派模型,由各自的类加载器来自行去加载的话,如果用户自定义了一个名为java.lang.Object类,并放在了类路径classpath下,那么程序中就会出现多个不同的Object类,JDK中的类就乱套了。
双亲委派模型的实现是非常简单的,来看java.lang.ClassLoader中的loadClass()方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先,检查请求的类是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载无法完成加载请求
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 在父类的加载器无法加载时,
// 再调用本身的findClass()方法进行加载
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
自定义类加载器
Java提供了一个抽象类加载器ClassLoader,所有的用户自定义类加载器都应该继承自这个抽象类
在代码中最常见的一个类加载器就是ClassLoader
,这个抽象类类加载器就是属于上述的三层中的“应用程序加载器”或“系统加载器”
在自定义ClassLoader的子类时,两种做法:
- 重写loadClass()方法
- 重写findClass()方法
建议:重写findClass(),因为在ClassLoader中的loadClass()中会调用findClass(),最好不要修改loadClass()的内部逻辑。
在双亲委派模型中,会先交给父类加载器去加载,如果父类加载器加载失败,则会调用findClass(),因此我们一般实现findClass()来实现子类的类加载逻辑,不去破坏父类的loadClass()方法
来看我们自定义的类加载器,实现加载磁盘上任意位置的Class文件
package com.liumingkai.domain;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
/**
* @author 刘明凯
* @version 0.0.1
* @date 2023年5月11日 13:29
*/
public class MyClassLoader extends ClassLoader {
private String path;
public MyClassLoader(String path) {
this.path = path;
}
public MyClassLoader(ClassLoader parent, String path) {
super(parent);
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;
try {
bis = new BufferedInputStream(new FileInputStream(path + name + ".class"));
baos = new ByteArrayOutputStream();
int len;
byte[] data = new byte[1024];
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
//获取内存中的完整的字节数组的数据
byte[] classByteArray = baos.toByteArray();
//将字节数组转换为Class的实例
return defineClass(null, classByteArray, 0, classByteArray.length);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != baos) {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
try {
if (null != bis) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
当定义完成了类加载器后,我们只需要在程序中调用类加载器的loadClass()来加载类即可
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("D://");
Class<?> helloClass = myClassLoader.loadClass("Hello");
System.out.println("类加载器是" + helloClass.getClassLoader());
Object obj = helloClass.newInstance();
System.out.println(obj);
}