本周,我的秋招正式进入到了面试阶段,不过现在来看,效果不尽人意,字节跳动一面开始的问题还比较简单,但却挂在了算法上,而面试阿里的时候的时候却感到了自己知识的深度与广度都还不够。其中一个问题便是:什么是双亲委派模型?嗯,这个背过。面试官接着又是一问,如何打破?这可没学过……
双亲委派模型
我们知道,java的类加载过程是:加载->验证->准备->解析->初始化->使用->卸载。
加载过程可以简单通过ClassLoader将.class文件加载jvm中。
同时,我们要知道,在Java中任意一个类都是由这个类本身和加载这个类的类加载器来确定这个类在JVM中的唯一性。也就是你用你A类加载器加载的com.aa.ClassA
和你A类加载器加载的com.aa.ClassA
它们是不同的,也就是用instanceof
这种对比都是不同的。所以即使都来自于同一个class文件但是由不同类加载器加载的那就是两个独立的类。
什么是双亲委派
首先需要说明的是,双亲委派这个名字是很容易让人误会的,因为并不能帮助人理解后面的加载过程,反而让人感觉晕。双亲委派的原文是"parents delegate"。parents在英文中是“父母”、“双亲”的意思,但其实表达的是“父母这一辈”的人的意思。实际上这个模型中,只是表达“父母这一辈”的class loader而已,所以所谓的双亲委派就是:
如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
在java中,ClassLoader有四种
- BootStrapClassLoader: c++编写,加载核心库java.*;
- Extension ClassLoader: java编写,加载扩展库javax.*;
- AppClassLoader: java编写吗,加载程序目录;
- 自定义ClassLoader:java编写,定制化加载;
使用双亲委派的好处
虚拟机只有在两个类的类名相同且加载该类的加载器均相同的情况下才判定这是一个类。若不采用双亲委派机制,同一个类有可能被多个类加载器加载,这样该类会被识别为两个不同的类,相互赋值时会有问题。
双亲委派机制能保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
其次是考虑到安全因素。假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
打破双亲委派模型
双亲委派模型不是一种强制性约束,也就是你不这么做也不会报错怎样的,它是一种JAVA设计者推荐使用类加载器的方式。
再想想双亲委派是怎么加载的,给父类,那我们自定义一个ClassLoader不给父类不就行了吗……当时的自己竟然没想到,还是学的不够透彻。
我们可以复写loadClass(),让其不向上委派
public class UnDelegationClassLoader extends ClassLoader {
private String classpath;
public UnDelegationClassLoader(String classpath) {
super(null);
this.classpath = classpath;
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> clz = findLoadedClass(name);
if (clz != null) {
return clz;
}
// jdk 目前对"java."开头的包增加了权限保护,这些包我们仍然交给 jdk 加载
if (name.startsWith("java.")) {
return ClassLoader.getSystemClassLoader().loadClass(name);
}
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
InputStream is = null;
try {
String classFilePath = this.classpath + name.replace(".", "/") + ".class";
is = new FileInputStream(classFilePath);
byte[] buf = new byte[is.available()];
is.read(buf);
return defineClass(name, buf, 0, buf.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
throw new IOError(e);
}
}
}
}
}
JDBC、Tomcat中为什么要破坏双亲委派模型
对本问题的详细研究可以看——阿里面试题:JDBC、Tomcat为什么要破坏双亲委派模型
JDBC
JDBC的Driver接口定义在JDK中,其实现由各个数据库的服务商来提供,比如MySQL驱动包。DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包,根据类加载机制,当被装载的类引用了另外一个类的时候,虚拟机就会使用装载第一个类的类装载器装载被引用的类。也就是说BootStrap类加载器还要去加载jar包中的Driver接口的实现类。我们知道,BootStrap类加载器默认只负责加载 JAVA_HOME中jre/lib/rt.jar 里所有的class,所以需要由子类加载器去加载Driver实现,这就破坏了双亲委派模型。
这个子类加载器是通过 Thread.currentThread().getContextClassLoader() 得到的线程上下文加载器。
在 sun.misc.Launcher 初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,所以线程上下文类加载器默认情况下就是系统加载器。
Tomcat
Tomcat的类加载器
每个Tomcat的webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。
事实上,tomcat之所以造了一堆自己的classloader,大致是出于下面三类目的:
- 对于各个
webapp
中的class
和lib
,需要相互隔离,不能出现一个应用中加载的类库会影响另一个应用的情况,而对于许多应用,需要有共享的lib以便不浪费资源。 - 与
jvm
一样的安全性问题。使用单独的classloader
去装载tomcat
自身的类库,以免其他恶意或无意的破坏; - 热部署。相信大家一定为
tomcat
修改文件不用重启就自动重新装载类库而惊叹吧。
小结
面完字节,感觉自己算法太弱了,得加强算法,面完阿里,感觉自己太弱了,咋这么多不会……秋招之路,任重道远。
希望学弟学妹们对算法还是重视起来,没事的时候可以刷刷leetcode,毕竟,如果想进大厂,手撕代码是免不了的。
对这次阿里面试的一个简单面经。
参考文章
版权声明
本文作者:河北工业大学梦云智开发团队 - 李宜衡
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。