一、前言

笔者曾经阅读过周志明的《深入理解Java虚拟机》这本书,阅读完后自以为对jvm有了一定的了解,然而当真正碰到问题的时候,才发现自己读的有多粗糙,也体会到只有实践才能加深理解,正应对了那句话——“Talk is cheap, show me the code”。前段时间,笔者同事提出了一个关于类加载器破坏双亲委派的问题,以我们常见到的数据库驱动Driver为例,为什么要实现破坏双亲委派,下面一起来重温一下。

二、双亲委派

想要知道为什么要破坏双亲委派,就要先从什么是双亲委派说起,在此之前,我们先要了解一些概念:

  • 对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性

什么意思呢?我们知道,判断一个类是否相同,通常用equals()方法,isInstance()方法和isAssignableFrom()方法。来判断,对于同一个类,如果没有采用相同的类加载器来加载,在调用的时候,会产生意想不到的结果:

public class DifferentClassLoaderTest {

    public static void main(String[] args) throws Exception {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream stream = getClass().getResourceAsStream(fileName);
if (stream == null) {
return super.loadClass(name);
}
try {
byte[] b = new byte[stream.available()];
// 将流写入字节数组b中
stream.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
} return super.loadClass(name);
}
};
Object obj = classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof DifferentClassLoaderTest); }
}

输出结果:

class jvm.DifferentClassLoaderTest
false

如果在通过classLoader实例化的使用,直接转化成DifferentClassLoaderTest对象:

DifferentClassLoaderTest obj = (DifferentClassLoaderTest) classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();

就会直接报 java.lang.ClassCastException:,因为两者不属于同一类加载器加载,所以不能转化!

2.1、为什么需要双亲委派

基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。

双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。

这里有几个流程要注意一下:

  1. 子类先委托父类加载
  2. 父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
  3. 子类在收到父类无法加载的时候,才会自己去加载

jvm提供了三种系统加载器:

  1. 启动类加载器(Bootstrap ClassLoader):C++实现,在java里无法获取,负责加载<JAVA_HOME>/lib下的类。
  2. 扩展类加载器(Extension ClassLoader): Java实现,可以在java里获取,负责加载<JAVA_HOME>/lib/ext下的类。
  3. 系统类加载器/应用程序类加载器(Application ClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。

附上三者的关系:

2.2、双亲委派的实现

双亲委派的实现其实并不复杂,其实就是一个递归,我们一起来看一下ClassLoader里的代码:

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步上锁
synchronized (getClassLoadingLock(name)) {
// 先查看这个类是不是已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 递归,双亲委派的实现,先获取父类加载器,不为空则交给父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
// 前面提到,bootstrap classloader的类加载器为null,通过find方法来获得
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
} if (c == null) {
// 如果还是没有获得该类,调用findClass找到类
long t1 = System.nanoTime();
c = findClass(name); // jvm统计
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 连接类
if (resolve) {
resolveClass(c);
}
return c;
}
}

三、破坏双亲委派

3.1、为什么需要破坏双亲委派?

因为在某些情况下父类加载器需要委托子类加载器去加载class文件。受到加载范围的限制,父类加载器无法加载到需要的文件,以Driver接口为例,由于Driver接口定义在jdk当中的,而其实现由各个数据库的服务商来提供,比如mysql的就写了MySQL Connector,那么问题就来了,DriverManager(也由jdk提供)要加载各个实现了Driver接口的实现类,然后进行管理,但是DriverManager由启动类加载器加载,只能记载JAVA_HOME的lib下文件,而其实现是由服务商提供的,由系统类加载器加载,这个时候就需要启动类加载器来委托子类来加载Driver实现,从而破坏了双亲委派,这里仅仅是举了破坏双亲委派的其中一个情况。

3.2、破坏双亲委派的实现

我们结合Driver来看一下在spi(Service Provider Inteface)中如何实现破坏双亲委派。

先从DriverManager开始看,平时我们通过DriverManager来获取数据库的Connection:

String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");

在调用DriverManager的时候,会先初始化类,调用其中的静态块:

static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
} private static void loadInitialDrivers() {
...
// 加载Driver的实现类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
}
return null;
}
});
...
}

为了节约空间,笔者省略了一部分的代码,重点来看一下ServiceLoader.load(Driver.class)

public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程中的上下文类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}

可以看到,load方法调用获取了当前线程中的上下文类加载器,那么上下文类加载器放的是什么加载器呢?

public Launcher() {
...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}

sun.misc.Launcher中,我们找到了答案,在Launcher初始化的时候,会获取AppClassLoader,然后将其设置为上下文类加载器,而这个AppClassLoader,就是之前上文提到的系统类加载器Application ClassLoader,所以上下文类加载器默认情况下就是系统加载器

继续来看下ServiceLoader.load(service, cl)

public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
} private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// ClassLoader.getSystemClassLoader()返回的也是系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
} public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

上面这段就不解释了,比较简单,然后就是看LazyIterator迭代器:

private class LazyIterator implements Iterator<S>{
// ServiceLoader的iterator()方法最后调用的是这个迭代器里的next
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
} private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
// 根据名字来加载类
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
} public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
} private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// 在classpath下查找META-INF/services/java.sql.Driver名字的文件夹
// private static final String PREFIX = "META-INF/services/";
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
} }

好了,这里基本就差不多完成整个流程了,一起走一遍:

四、总结

Driver剩余的加载过程就省略了,有兴趣的园友可以继续深入了解一下,不得不说,jvm博大精深,看起来容易,真正到了用起来才发现各种问题,也只有实践才能加深理解,最后谢谢各位园友观看,如果有描述不对的地方欢迎指正,与大家共同进步!

参考部分:

【JVM】浅谈双亲委派和破坏双亲委派的更多相关文章

  1. JVM浅谈

    **前言** 由于先前也遇到过一些性能问题,OOM算是其中的一大类了.因此也对jvm产生了一些兴趣.自己对jvm略做了些研究.后续继续补充. **从oom引申出去** 既然说到oom,首先需要知道oo ...

  2. 结合JVM 浅谈Java 类加载器(Day_03)

    所谓错过,不是错了,而是过了. 什么是JAVA类加载? Class对象由JVM自动产生,每当一个类被加载时,JVM就自动为其生成一个Class对象,通过Class对象可以获得类的相关信息.将类信息读取 ...

  3. # 【jvm】01-双亲委派都会说,破坏双亲委派你会吗

    [jvm]01-双亲委派都会说,破坏双亲委派你会吗 欢迎关注b站账号/公众号[六边形战士夏宁],一个要把各项指标拉满的男人.该文章已在github目录收录. 屏幕前的大帅比和大漂亮如果有帮助到你的话请 ...

  4. JDBC、Tomcat为什么要破坏双亲委派模型?

    问题一:双亲委派模型是什么 如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的 ...

  5. Tomcat类加载器破坏双亲委派

    转载:https://blog.csdn.net/qq_38182963/article/details/78660779 http://www.cnblogs.com/aspirant/p/8991 ...

  6. 如何把Java代码玩出花?JVM Sandbox入门教程与原理浅谈

    在日常业务代码开发中,我们经常接触到AOP,比如熟知的Spring AOP.我们用它来做业务切面,比如登录校验,日志记录,性能监控,全局过滤器等.但Spring AOP有一个局限性,并不是所有的类都托 ...

  7. ClassLoader类加载器 & Java类加载机制 & 破坏双亲委托机制

    ClassLoader类加载器 Java 中的类加载器大致可以分成两类: 一类是系统提供的: 引导类加载器(Bootstrap classloader):它用来加载 Java 的核心库(如rt.jar ...

  8. 破坏双亲委托机制的一些情况---Tomcat和JDBC,破坏后的安全问题

    采用双亲委托机制的原因 类加载器就是将字节码搬进jvm方法区的组件.我们知道,JVM识别加载进来的类是通过类加载器+类全名完成的,也就是说同一个类由不同类加载器加载进去的话就会被视为不同的类.jdk提 ...

  9. JAVA基础|从Class.forName初始化数据库到SPI破坏双亲委托机制

    代码托管在:https://github.com/fabe2ry/classloaderDemo 初始化数据库 如果你写过操作数据库的程序的话,可能会注意,有的代码会在程序的开头,有Class.for ...

随机推荐

  1. golang web框架 beego 学习 (二) router and controller

    1 Router和Controller的常用配置 beego.Router("/user/admin", &controllers.UserController{}) // ...

  2. C++多线程同步之临界区(CriticalSection)

    原文链接:http://blog.csdn.net/olansefengye1/article/details/53262917 一.Win32平台 1.相关头文件和接口 #include <w ...

  3. 香侬科技独家对话Facebook人工智能研究院首席科学家Devi Parikh

    Facebook 人工智能研究院(FAIR)首席科学家 Devi Parikh 是 2017 年 IJCAI 计算机和思想奖获得者(IJCAI 两个最重要的奖项之一,被誉为国际人工智能领域的「菲尔兹奖 ...

  4. python之路05

    一  元组 对于元组我们可以把他看成一个不可变的列表# 元组:在()内用逗号分隔开的能够存多个值,对于元组来说列表有的一些功能它基本上都有,# 1.按索引取值(正向取+反向取):只能取# 2.切片(顾 ...

  5. java在注解中绑定方法参数的解决方案

    我们有这样子的需求,需要记录用户操作某个方法的信息并记录到日志里面,例如,用户在保存和更新任务的时候,我们需要记录下用户的ip,具体是保存还是更新,调用的是哪个方法,保存和更新的任务名称以及操作是否成 ...

  6. python3解析XML文件

    软硬件环境 Ubuntu 15.10 32bit Python 3.5.1 PyQt 5.5.1 前言 Python解析XML的方法挺多,本文主要是利用ElementTree来完成. 实例讲解 解析X ...

  7. PHP提取多维数组指定一列的方法大全

    目录 1 array_column函数法 2 array_walk函数法 3 array_map函数法 4 foreach循环法 5 array_map变种 PHP中对多维数组特定列的提取,是个很常用 ...

  8. 框架之Tornado(简单介绍)

    引言 回想Django的部署方式 以Django为代表的python web应用部署时采用wsgi协议与服务器对接(被服务器托管),而这类服务器通常都是基于多线程的,也就是说每一个网络请求服务器都会有 ...

  9. HP服务器安装配置教程

    使用iLO远程管理HP系列服务器 http://blog.51cto.com/wangchunhai/837529

  10. oracle11gr2笔记(一)

    一,使用scoot用户被锁.解决办法:(http://ciiiso.blog.51cto.com/8779682/1432869/) 二,使用root用户登录系统无法sqlplus,提示说permis ...