可先阅读:JVM类加载器
文章目录
1 双亲委托机制介绍
类加载器最重要的机制——双亲委托机制,有时候也称为父委托机制。当一个类加载器被调用了loadClass之后,它并不会直接将其加载,而是先交给当前类加载器的父加载器尝试加载直到最顶层的父加载器,然后再依次向下进行加载
loadClass方法源码:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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) {
long t0 = System.nanoTime();
try {
// 如果当前类存在父类加载器,则调用父类加载器的loadClass(name,false)方法对其进行加载。
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 如果当前类的所有父类加载器都没有成功加载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();
}
}
// 由于loadClass指定了resolve为false,所以不会进行连接阶段的继续执行,这也就解释了为什么通过类加载器加载类并不会导致类的初始化。
if (resolve) {
resolveClass(c);
}
return c;
}
}
- 从当前类加载器的已加载类缓存中根据类的全路径名查询是否存在该类,如果存在则直接返回。
- 如果当前类存在父类加载器,则调用父类加载器的loadClass(name,false)方法对其进行加载。
- 如果当前类加载器不存在父类加载器,则直接调用根类加载器对该类进行加载。
- 如果当前类的所有父类加载器都没有成功加载class,则尝试调用当前类加载器的findClass方法对其进行加载,该方法就是我们自定义加载器需要重写的方法。
- 最后如果类被成功加载,则做一些性能数据的统计。
- 由于loadClass指定了resolve为false,所以不会进行连接阶段的继续执行,这也就解释了为什么通过类加载器加载类并不会导致类的初始化。
可见先回调用父加载器,如果父加载器没有加载,最后通过自己加载类
为了更清楚的认识一下:之前的测试自定义类加载器的时候,需要保证测试类的classpath下没有要加载的类:自定义类加载器
现在在测试类的classpath下,也定义一个HelloWorld类
运行测试类发现输出类加载器已经不是自定义的加载器了
类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello World Class is Initialized.
Hello World
因为自定义的类加载器默认的父加载器是AppClassLoader,而AppClassLoader就是主要就是加载classpath下的类,而当前classpath下存在这么一个类,所以优先被父加载器加载了,而不会被我们自定义的类加载器加载。
当前classpath下不存在这么一个类,还是优先被父加载器加载,但是父加载器此时无法加载到这个类,就会被自定义加载器去加载。
如果你理解了,想想如何即使classpath存在HelloWorld这个类,使测试代码
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
MyClassLoader classLoader = new MyClassLoader();
// 这里还是使用MyClassLoader 加载
Class<?> aClass = classLoader.loadClass("study.wyy.thread.join.HelloWorld");
System.out.println("类加载器:" + aClass.getClassLoader());
Object helloWorld = aClass.newInstance();
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println(result);
}
的Class<?> aClass = classLoader.loadClass("study.wyy.thread.join.HelloWorld");
还是使用MyClassLoader 加载呢?
- 绕过系统类加载器:如果设置了MyClassLoader的父加载器是ExtClassLoader,而ExtClassLoader是不会加载classpath下的类,所以父加载器又无法加载,那么又会交给我们自定义的MyClassLoader加载
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
// 获取ExtClassLoader
ClassLoader parent = MyClassLoaderTest.class.getClassLoader().getParent();
// 设置父加载器是ExtClassLoader
String classDir = System.getProperty("user.home") + "/MyClassLoader";
MyClassLoader classLoader = new MyClassLoader(classDir,parent);
Class<?> aClass = classLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
System.out.println("类加载器:" + aClass.getClassLoader());
Object helloWorld = aClass.newInstance();
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println(result);
}
输出:
类加载器:My ClassLoader
Hello World Class is Initialized.
Hello World
- 直接设置父加载器是null:如果当前类加载器不存在父类加载器,则直接调用根类加载器对该类进行加载。根类加载器也不会加载classpath下的类
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
String classDir = System.getProperty("user.home") + "/MyClassLoader";
MyClassLoader classLoader = new MyClassLoader(classDir,null);
Class<?> aClass = classLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
System.out.println("类加载器:" + aClass.getClassLoader());
Object helloWorld = aClass.newInstance();
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println(result);
}
类加载器:My ClassLoader
Hello World Class is Initialized.
Hello World
2 破坏双亲委托机制
类加载器的父委托机制的逻辑主要是由loadClass来控制的,有些时候我们需要打破这种双亲委托的机制,比如HelloWorld这个类就是不希望通过系统类加载器对其进行加载。虽然在上面给出了两种解决方案,但是采取的都是绕过ApplicationClassLoader的方式去实现的,并没有避免一层一层的委托,那么有没有办法可以绕过这种双亲委托的模型呢?
JDK提供的双亲委托机制并非一个强制性的模型,程序开发人员是可以对其进行灵活发挥破坏这种委托机制的,比如我们想要在程序运行时进行某个模块功能的升级,甚至是在不停止服务的前提下增加新的功能,这就是我们常说的热部署。热部署首先要卸载掉加载该模块所有Class的类加载器,卸载类加载器会导致所有类的卸载,很显然我们无法对JVM三大内置加载器进行卸载,我们只有通过控制自定义类加载器才能做到这一点。
既然双亲委派逻辑就是在loadClass方法实现的,想打破那就重写该方法:
下面的代码大部分和之前的自定义的类加载器一样,只是重写了loadClass方法。
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author wyaoyao
* @date 2021/4/4 16:27
* 打破双亲委派机制演示:重写loadClass
*
*/
public class BrokerDelegateClassLoader extends ClassLoader{
/**
* 定义默认加载class文件的目录
*/
private final static Path DEFAULT_CLASS_DIR = Paths.get(System.getProperty("user.home"), "MyClassLoader");
private final Path classDir;
public BrokerDelegateClassLoader() {
super();
this.classDir = DEFAULT_CLASS_DIR;
}
public BrokerDelegateClassLoader(String classDir) {
super();
this.classDir = Paths.get(classDir);
}
/**
* 指定class路径的同时,指定父类加载器
*
* @param classDir
* @param parent
*/
public BrokerDelegateClassLoader(String classDir, ClassLoader parent) {
super(parent);
this.classDir = Paths.get(classDir);
}
/**
* 指定父类加载器
*
* @param parent
*/
public BrokerDelegateClassLoader(ClassLoader parent) {
super(parent);
this.classDir = DEFAULT_CLASS_DIR;
}
/**
* 重写findClass方法
*
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 将class文件读入内存
byte[] classByte = this.readClassByte(name);
// 如果数据为null,或者没有读到任何信息,则抛出ClassNotFoundException异常
if (null == classByte || classByte.length == 0) {
throw new ClassNotFoundException("Can not load the class " + name);
}
// 调用defineClass方法定义class
return this.defineClass(name, classByte, 0, classByte.length);
}
/**
* 读取clas文件
*
* @param name
* @return
*/
private byte[] readClassByte(String name) throws ClassNotFoundException {
// 将包名分隔符转为文件路径分隔符
String classPath = name.replace(".", "/");
Path classFullPath = classDir.resolve(Paths.get(classPath + ".class"));
if (!classFullPath.toFile().exists()) {
throw new ClassNotFoundException("The class" + name + "not find");
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
Files.copy(classFullPath, baos);
return baos.toByteArray();
} catch (IOException e) {
throw new ClassNotFoundException("load the class " + name + " occur error.", e);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 根据类的全路径名称进行加锁,确保每一个类在多线程的情况下只被加载一次。
synchronized (getClassLoadingLock(name)) {
// 到已加载类的缓存中查看该类是否已经被加载,如果已加载则直接返回。
Class<?> clazz = findLoadedClass(name);
// 若缓存中没有被加载的类,则需要对其进行首次加载
if (null == clazz) {
// 则需要对其进行首次加载,如果类的全路径以java和javax开头,则直接委托给系统类加载器对其进行加载。
if (name.startsWith("java.") || name.startsWith("javax")) {
try {
clazz = getSystemClassLoader().loadClass(name);
} catch (Exception e) {
//ignore
}
} else {
try {
// 交给自定义类加载器完成对类的加载 (先交给自己,而不是交给父加载器)
clazz = this.findClass(name);
} catch (ClassNotFoundException e) {
//ignore
}
// 若自定义类加载仍旧没有完成对类的加载,则委托给其父类加载器进行加载或者系统类加载器进行加载。
if (null == clazz) {
// 获取父加载器,使用父加载器加载
ClassLoader parent = getParent();
if (parent != null) {
clazz = parent.loadClass(name);
} else {
// 如果父加载器是null,就使用系统加载器加载
clazz = getSystemClassLoader().loadClass(name);
}
}
}
}
if (clazz == null) {
throw new ClassNotFoundException("The class " + name + " not found.");
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
}
测试:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
BrokerDelegateClassLoader classLoader = new BrokerDelegateClassLoader();
Class<?> aClass = classLoader.loadClass("study.wyy.thread.jvm.HelloWorld");
System.out.println("类加载器:" + aClass.getClassLoader());
Object helloWorld = aClass.newInstance();
Method welcomeMethod = aClass.getMethod("welcome");
String result = (String) welcomeMethod.invoke(helloWorld);
System.out.println(result);
}
类加载器:study.wyy.thread.jvm.BrokerDelegateClassLoader@6e0be858
Hello World Class is Initialized.
Hello World
此时在测试的时候,就不要再classPath下删除Helloworld这个类了