java 类加载之双亲委派
作为一名java程序员,肯定都会知道类的加载过程分为:加载、链接、初始化,不知道也没有关系,可以关注我公众号去找「java类加载过程」这篇文章进行查看,本篇主要说一下加载的第一步「装载」的详细过程-双亲委派机制。
-
什么是双清委派机制
我个人将双亲委派机制理解成八个字:向上查找、向下委派。
简单的画个图来解释一下:
简单理解就是:一个类需要加载,会首先使用这个类的全命名不断的检查是否已经加载过。如果加载过就不允许加载,如果没有加载过那么就会尝试去加载,不过每一个类加载器都有自己固定的活动范围,所以找不到这个类的话就会委派给下一级的类加载器去加载,直到最后一级如果都加载不到,就会报classNotFoundException。
-
好了上面大体上解释了了什么是双亲委派机制,下面我们就需要扒开他的衣服好好看看里面
首先了解几个类加载器:
- Application ClassLoader:应用程序类加载器。这个类加载器是我们最为常见的,我们写的代码、从外部引入的类默认都是这个类加载器帮我们加载进去的
- Extension ClassLoader:扩展类加载器。他就只负责加载**<JAVA_HOME>/lib/ext**文件夹下的类
- Bootstrap ClassLoader:启动类加载器。他就更神秘了,他只负责加载**<JAVA_HOME>/lib/**文件夹下的类,而且他是c++实现的,我们还获取不到他。
- ** ClassLoader:自定义类加载器。这个是我们自己实现的,可以指定加载某个类。只需要继承ClassLoader类,然后重写findClass方法就OK了。
为什么需要双亲委派机制?
为了安全。可以想象一个场景:你自己写一个java.lang.Stirng类,加载到项目中,然后在里面为所欲为,是不是就炸了。
当然也有打破双亲委派机制的地方,比如说tomcat。它里面有很多歌application,如果不能同时加载,那就意味着一个tomcat只能跑一个项目了。
@Slf4j
public class MyClassLoader {
public static void main(String[] args) {
log.info("String 的 类加载器:" + String.class.getClassLoader());
log.info("MyClassLoader 的 类加载器:" + MyClassLoader.class.getClassLoader());
log.info("NashornBeansLinker 的 类加载器:" + NashornBeansLinker.class.getClassLoader());
log.info("AppClassLoader 的 上级加载器:" + MyClassLoader.class.getClassLoader().getParent());
log.info("ExtClassLoader 的 上级加载器:" + NashornBeansLinker.class.getClassLoader().getParent());
}
}
通过这个小程序我们也能够看出这些类加载器的不同。
当然也能看出一点,就是他们之间并不是父子关系,看到我日志里面的用词上级了没有,为了不与父加载器弄混淆。
下面我们简单的看一下加载类的源码就能清楚了:
/**
*
* 使用指定的 二进制名称 加载类。此方法的 默认实现按以下的顺序搜索类:
*
* 1、调用 findLoadedClass(String) 以检查该类是否已加载。
* 2、在父类加载器上调用 loadClass(String) loadClass 方法。如果父程序为 null ,则使用内置到虚拟机的类加载器。
* 3、调用 findClass(String) 方法(当然这个findClass需要自己实现,默认的直接抛出ClassNotFoundException)查找该类。
* 如果使用上述步骤找到了类,并且解析(resolve)标志为true,则此方法将在结果的 class 对象上调用 resurveClass(Class) 方法。
* 「resurveClass方法其实就是将类进行链接,其实就是类加载过程的第二大步中的第三小步骤」
*
* 实现这个类的子类最好的是重写findClass(String) 而不是 这个方法,为什么呢?因为重写这个方法会打破双亲委派机制
*
* 除非重写,否则此方法在整个类加载过程中都是同步的
*
*/
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 {
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();
//这个类就是需要我们自己去实现的类,它定义好了钩子方法(设计模式中的模板方法模式)
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;
}
}
我简单的把注释给翻译了一遍,在这里不得不感慨一下,别人的源码的注释是真的好,看了注释之后源码就了解了大半。还有就是学习真的需要看源码。虽然一开始很痛苦,但是看懂了之后真的有很大益处。这边博客我就干了接近三天,都耗在源码上了。
好了不扯了,回到正题
注意上面源码中的两点:c = parent.loadClass(name, false);
,这个就是不断的向上检查并加载的过程所在,也能看出来每个加载器之间并不是父子集成关系,而是在里面维护了一个引用:
从上可以看到,ClassLoader初始化的时候就会将加载器的parent引用组装好了。
第二个就是需要注意的就是这个:
//这个类就是需要我们自己去实现的类,它定义好了钩子方法(设计模式中的模板方法模式) c = findClass(name);
也就是说我们可以实现自己的类加载器,然后重写findClass方法就可以了,话不多说,show the code:
我在桌面随便新建一个HelloWorlds.java ,里面只有一个main方法,打印hello world,然后将它编译成class文件。自己写一个加载器将它加载:
运行之后可以发现打印出来的类加载器就是我们自己写的MyClassLoader。
package com.example.studydemo.jvm;
/**
* @program: studydemo
* @description: jvm学习之类加载器(双亲委派机制)
* @author: kby
* @create: 2023-09-03 20:40
*/
@Slf4j
public class MyClassLoader extends ClassLoader {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass("HelloWorlds");
System.out.println("HelloWorlds 的类加载器是:" + aClass.getClassLoader());
Method main = aClass.getDeclaredMethod("main", String[].class);
main.invoke(null,(Object) new String[0]);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//找到class的绝对路径
String fileName = "/Users/mac/Desktop/"+name.replaceAll("\\.",File.separator)+".class";
File f = new File(fileName);
FileInputStream fileInputStream = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
fileInputStream = new FileInputStream(f);
byte[] bytes = new byte[(int)f.length()];
fileInputStream.read(bytes);//将文件读到字节数组中
return defineClass(name, bytes, 0, bytes.length);//将字节数组转化成类
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return super.findClass(name);
}
}
总结一下
今天我们主要讲类的双亲委派加载机制:
- 几种类加载器,以及他们加载的作用域
- 类加载器关于加载方法的源码
- 自己实现自己的类加载器