相信不少的同学在面试的时候会被问到一个词:双亲委派,懂得同学懂,不懂的同学可能会尴尬一笑,那么今天咱们就来聊聊这个问题的原理,首先我们需要了解一下java中常见的几种类加载器。
1.BootstrapClassLoader
纯C++实现的类加载器,没有对应的Java类,主要加载的是jre/lib/目录下的核心库
2.ExtClassLoader
类的全名是sun.misc.Launcher$ExtClassLoader,主要加载的是jre/lib/ext/目录下的扩展包
3.AppClassLoader
类的全名是sun.misc.Launcher$AppClassLoader,主要加载的是CLASSPATH路径下的包
下面我们通过一些简单代码示例来验证一下上述的结论
首先创建一个Java类,在main方法里添加以下代码:
public class ClassLoaderTest {
public static void main(String[] args){
//1.打印ClassLoaderTest的类加载器
Class mainClass = ClassLoaderTest.class;
ClassLoader mainLoader = mainClass.getClassLoader();
System.out.println("mainLoader's Name : "+mainLoader.toString());
}
}
我们在代码里得到类的一个class对象,然后通过它的getClassLoader方法得到一个ClassLoader对象,那么运行一下看看这个ClassLoader对象的名字是什么
可以看到红框里的类是AppClassLoader,路径是sun.misc.Launcher下的。
我们点进ClassLoader的源码中发现
它有个字段parent,返回值也是一个ClassLoader,并且提供了方法
那么我们通过这个字段打印一下parent对应的ClassLoader的名字,在代码中添加:
//2.通过getParent方法,获取mainLoader中的parent字段
ClassLoader parentLoader = mainLoader.getParent();
System.out.println("parentLoader's Name : "+parentLoader.toString());
运行看看结果:
可以看到AppClassLoader的parent得到的是ExtClassLoader,我们现在来打印一下ExtClassLoader的加载路径
//3.打印ExtClassLoader的加载路径
URL[] mUrlsExt = ((URLClassLoader)parentLoader).getURLs();
print(mUrlsExt);
/**
* 打印url数组
* @param urls
*/
public static void print(URL[] urls){
for (URL url : urls){
System.out.println(url);
}
}
可以看到输出结果,ExtClassLoader的加载路径加载的是jre/lib/ext/目录下的扩展包。
接下来我们对parentLoader调用getParent方法打印它对父加载器:
//4.通过gerParent方法,获取parentLoader中parent字段
ClassLoader parentLoader2 = parentLoader.getParent();
System.out.println("parentLoader2's Name : "+parentLoader2);
因为BootstrapClassLoader是C++实现的,所以它没有对应的Java类,所以我们只能采取一些特殊手段来获取它的加载路径,前面我们发现AppClassLoader和ExtClassLoader都是Launcher这个类的内部类,而且Launcher提供了一个方法getBootstrapClassPath来获取BootstrapClassLoader的加载路径,看代码:
//5.打印BootstrapClassLoader的加载路径
try {
Class launcherClass = Class.forName("sun.misc.Launcher");
Method methodGetClassPath = launcherClass.getDeclaredMethod("getBootstrapClassPath",null);
if (null != methodGetClassPath){
methodGetClassPath.setAccessible(true);
Object object = methodGetClassPath.invoke(null,null);
if (null != object){
Method methodGetUrls = object.getClass().getDeclaredMethod("getURLs",null);
if (null != methodGetUrls){
methodGetUrls.setAccessible(true);
URL[] mUrlBoot = (URL[]) methodGetUrls.invoke(object,null);
print(mUrlBoot);
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
运行结果:
可以看到BootstrapClassLoader的加载路径是jre/lib目录,加载的是jre/lib/目录下的核心库,和开头说的也是一致的。
现在我们要来介绍一下,一个类加载器是如何加载一个类的。
首先看一段代码:
public class ClassLoaderDemo {
public static void main(String[] args){
Class clazz = ClassLoaderDemo.class;
ClassLoader loader = clazz.getClassLoader();
System.out.println("loader's Name : "+loader.toString());
}
}
这段代码的运行结果如果看完上面并了解的同学应该能很准确的说出结果:AppClassLoader
下面在来加一点代码:
Class listClass = List.class;
ClassLoader listLoader = listClass.getClassLoader();
System.out.println("listLoader's Name : "+listLoader.toString());
可以看到List的classLoader为空,那么为什么为空呢?
因为像List是属于jdk中的东西,而jdk其实是放在一个rt.jar包中,而这个包的路径是:
jre/lib/目录下,通过上面说的,jre/lib/这个目录的jar包应该是由BootstrapClassLoader负责加载的,而这个BootstrapClassLoader类加载器是C++实现的,没有对应的Java类,所以打印出的结果为null。
接下来我们查看一下Java中ClassLoader这个类的源码是如何加载一个类的。
在ClassLoader源码中有一个loadClass方法,调用的是重载方法
我们需要重点关注红框部分的1-4这些代码,接下来捋一下逻辑:
1.第一步:走到代码1检查是否已经加载过这个类,如果加载过就直接返回,不走c==null逻辑里的代码,流程结束。
2.第二步:如果没加载过则进入到c == null的逻辑判断里,判断parent是否为空,如果不为空,就交由parent执行loadClass操作,否则执行findBootstrapClassOrNull方法。
这里我们以之前的例子来一步步看:
1)如果是AppClassLoader类加载器,执行loadClass方法时,parent不为空,parent是ExtClassLoader,现在由ExtClassLoader执行loadClass方法也就是代码2,继续走到里面后,判断parent是否为null,因为ExtClassLoader的parent==null,所以会走到代码3。
2)走到代码3后,执行findBootstrapClassOrNull方法,在该方法中调用findBootstrapClass方法,注意一个修饰符native,说明这个是native方法,因为BootstrapClassLoader是C++实现的,所以这里可以理解了。
3)如果前面代码1,2,3都执行完了,执行过程中出现了异常,这个时候c == null,会走到代码4中,执行findClass方法
findClass方法中什么都没做,只是抛出一个ClassNotFoundException异常,相信这个异常大家见得也比较多了。
整个的loadClass流程已经执行完了,用一张图来总结一下流程
由一段代码入手,接着刚才的代码,我们创建一个包,名为java.util,在这个包名下创建一个类ArrayList,写一段静态代码,然后在main方法中饮用ArrayList这个类,运行看看结果:
public class ClassLoaderDemo {
static {
System.out.println("ClassLoaderDemo is load!");
}
public static void main(String[] args){
Class clazz = ClassLoaderDemo.class;
ClassLoader loader = clazz.getClassLoader();
System.out.println("loader's Name : "+loader.toString());
Class listClass = ArrayList.class;
ClassLoader listLoader = listClass.getClassLoader();
System.out.println("listLoader's Name : "+listLoader.toString());
}
}
自己创建的ArrayList
package java.util;
public class ArrayList {
static {
System.out.println("ArrayList is load!");
}
}
运行结果:
可以看到这里并没有加载我们工程里的ArrayList,还是加载的jdk里的ArrayList,及时我们的工程里创建里一个和jdk里完全一样的类,仍然不会被加载。
那么这样由什么好处呢?
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
好啦,写到这里关于Java中常见的三种类加载器还有双亲委派(父委托)机制的说明已经介绍完了,希望对大家有所帮助!