本文来说下为什么要破坏JVM的双亲委派模型

文章目录

  • 概述
  • 双亲委派模型
  • 破坏双亲委派模型
    • 不使用Java SPI
    • 使用Java SPI
  • 自定义类加载器
    • 为什么要自定义类加载器
    • ClassLoader实现
    • 如何自定义类加载器
    • 自定义加载器的应用
  • 本文小结

概述

我原来面试的时候被问过一个这样的问题,「如果在你项目中建一个java.lang.String的类,那系统中用的String类是你定义的String类,还是原生api中的String类?」你可以试一下,发现最终系统中用的还是原生api中的String类,为什么会出现这种情况呢?这还得从类的加载过程说起。

我们都知道Java是跨平台的,是因为不同平台下的JVM能将字节码文件解释为本地机器指令,JVM是怎么加载字节码文件的?答案就是ClassLoader,先来打印看一下ClassLoader对象

public class ClassLoaderDemo1 {public static void main(String[] args) {// nullSystem.out.println(String.class.getClassLoader());ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();while (loader != null) {// sun.misc.Launcher$AppClassLoader@58644d46// sun.misc.Launcher$ExtClassLoader@7ea987acSystem.out.println(loader);loader = loader.getParent();}}
}

双亲委派模型

要理解这个输出,我们就得说一下双亲委派模型,「如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式」,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成。「双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码」。

采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以「避免类的重复加载」,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。

其次是考虑到安全因素,java核心api中定义类型不会被随意替换」,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

检查和加载过程以及系统提供的ClassLoader的作用如下图。文章一开始的问题,用双亲加载来解释就很容易理解用的是原生api中的String类。

类加载器的关系如下:

  • 启动类加载器,由C++实现,没有父类。
  • 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
  • 应用程序类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
  • 用户自定义类加载器,父类加载器肯定为AppClassLoader。自定义类加载器,父类加载器肯定为AppClassLoader。

破坏双亲委派模型

但是由于加载范围的限制,顶层的ClassLoader无法访问底层的ClassLoader所加载的类。所以此时需要破坏双亲委派模型」,以JDBC为例,讲一下为什么要破坏双亲委派模型。


不使用Java SPI

当我们刚开始用JDBC操作数据库时,你一定写过如下的代码。先加载驱动实现类,然后通过DriverManager获取数据库链接

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.
getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");
public class Driver extends NonRegisteringDriver implements java.sql.Driver {public Driver() throws SQLException {}static {try {DriverManager.registerDriver(new Driver());} catch (SQLException var1) {throw new RuntimeException("Can't register driver!");}}
}

在Driver类中向DriverManager注册对应的驱动实现类


使用Java SPI

在JDBC4.0以后,开始支持使用SPI的方式来注册这个Driver,具体做法就是在mysql的jar包中的META-INF/services/java.sql.Driver 文件中指明当前使用的Driver是哪个。

「SPI就是策略模式,根据配置来决定运行时接口的实现类是哪个」


这样当使用不同的驱动时,我们不需要手动通过Class.forName加载驱动类,只需要引入相应的jar包即可」。于是上面的代码就可以改成如下形式

Connection conn = DriverManager.
getConnection("jdbc:mysql://myhost/test?useUnicode=true&characterEncoding=utf-8&useSSL=false", "test", "test");

「那么对应的驱动类是何时加载的呢?」

  • 我们从META-INF/services/java.sql.Driver文件中获取具体的实现类“com.mysql.jdbc.Driver”
  • 通过Class.forName(“com.mysql.jdbc.Driver”)将这个类加载进来

DriverManager是在rt.jar包中,所以DriverManager是通过启动类加载器加载进来的。而Class.forName()加载用的是调用者的ClassLoader,所以如果用启动类加载器加载com.mysql.jdbc.Driver,肯定加载不到(「因为一般情况下启动类加载器只加载rt.jar包中的类哈」)。

「如何解决呢?」

想让顶层的ClassLoader加载底层的ClassLoader,只能破坏双亲委派机制。来看看DriverManager是怎么做的

DriverManager加载时,会执行静态代码块,在静态代码块中,会执行loadInitialDrivers方法。而这个方法中会加载对应的驱动类。

public class DriverManager {static {loadInitialDrivers();println("JDBC DriverManager initialized");}private static void loadInitialDrivers() {// 省略部分代码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) {// Do nothing}return null;}});// 省略部分代码}}

我们就看他使用的是哪种类型的ClassLoader,可以看到通过执行Thread.currentThread().getContextClassLoader()获取了线程上下文加载器

线程上下文类加载器可以通过Thread.setContextClassLoader()方法设置,默认是应用程序类加载器(AppClassLoader)

// ServiceLoader#load
public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}

ExtClassLoader和AppClassLoader都是通过Launcher类来创建的,在Launcher类的构造函数中我们可以看到线程上下文类加载器默认是AppClassLoader

public class Launcher {public Launcher() {Launcher.ExtClassLoader var1;try {var1 = Launcher.ExtClassLoader.getExtClassLoader();} catch (IOException var10) {throw new InternalError("Could not create extension class loader", var10);}try {this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);} catch (IOException var9) {throw new InternalError("Could not create application class loader", var9);}// 设置线程上下文类加载器为AppClassLoaderThread.currentThread().setContextClassLoader(this.loader);// 省略部分代码}
}

很明显,线程上下文类加载器让父类加载器能通过调用子类加载器来加载类,这打破了双亲委派模型的原则


自定义类加载器

为什么要自定义类加载器

为什么要自定义类加载器


ClassLoader实现

Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类」,所以我们先看一下ClassLoader的逻辑,看一下它加载类的主要逻辑。

截取了3个重要的方法

  • loaderClass:实现双亲委派
  • findClass:用来复写加载,即根据传入的类名,返回对应的Class对象
  • defineClass:本地方法,最终加载类只能通过defineClass
// 从这方法开始加载
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对象,找到就不用重新加载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// 这里体现了自顶向下尝试加载类,当父类加载加载不到时// 会抛出ClassNotFoundException// 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();// 如果都没有找到,通过自己的实现的findClass去加载// findClass方法没有找到会抛出ClassNotFoundExceptionc = findClass(name);// this is the defining class loader; record the statssun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);sun.misc.PerfCounter.getFindClasses().increment();}}// 是否需要在加载时进行解析if (resolve) {resolveClass(c);}return c;}
}

findClass用来复写加载

protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}

如何自定义类加载器

Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类

在自定义类加载器的时候,常见的做法有如下两种

  • 重写loadClass方法
  • 重写findClass方法

但是一般情况下重写findClass方法即可,不重写loadClass方法。」 因为loadClass是用来实现双亲委派模型的地方,修改这个方法会造成模型被破坏,容易操作问题。所以我们一般情况下重写findClass方法即可,根据传入的类名,返回对应的Class对象

下面我们就自己手写一个ClassLoader,从指定文件中加载class文件

public class DemoObj {public String toString() {return "I am DemoObj";}}

javac生成相应的class文件,放到指定目录,然后由FileClassLoader去加载

public class FileClassLoader extends ClassLoader {// class文件的目录private String rootDir;public FileClassLoader(String rootDir) {this.rootDir = rootDir;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {byte[] classData = getClassData(name);if (classData == null) {throw new ClassNotFoundException();} else {return defineClass(name, classData, 0, classData.length);}}private byte[] getClassData(String className) {String path = rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";try {InputStream ins = new FileInputStream(path);ByteArrayOutputStream baos = new ByteArrayOutputStream();int bufferSize = 4096;byte[] buffer = new byte[bufferSize];int bytesNumRead = 0;while ((bytesNumRead = ins.read(buffer)) != -1) {baos.write(buffer, 0, bytesNumRead);}return baos.toByteArray();} catch (IOException e) {e.printStackTrace();}return null;}public static void main(String[] args) {String rootDir = "/Users/peng/study-code/java-learning/src/main/java";FileClassLoader loader = new FileClassLoader(rootDir);try {// 传入class文件的全限定名Class<?> clazz = loader.loadClass("com.javashitang.classloader.DemoObj");// com.javashitang.classloader.FileClassLoader@1b28cdfaSystem.out.println(clazz.getClassLoader());// I am DemoObjSystem.out.println(clazz.newInstance().toString());} catch (Exception e) {e.printStackTrace();}}
}

自定义加载器的应用

可以对class文件进行加密和揭秘,实现应用的热部署,实现应用隔离等。以Tomcat中的ClassLoader为例演示一下。在解释防止类重名作用前先抛出一个问题,「Class对象的唯一标识能否只由全限定名确定?」。答案是不能,因为你无法保证多个项目间不出现相同全限定名的类。

「JVM判断2个类是否相同的条件是」

  • 全限定名相同
  • 由同一个类加载器加载

我们用上面写的FileClassLoader来验证一下

String rootDir = "/Users/peng/study-code/java-learning/src/main/java";
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);Class class1 = loader1.findClass("com.javashitang.classloader.DemoObj");
Class class2 = loader2.findClass("com.javashitang.classloader.DemoObj");// false
System.out.println(class1 == class2);

运行时数据区的分布如下


Tomcat中就定义了很多ClassLoader来实现应用的隔离

在Tomcat中提供了一个Common ClassLoader,它主要负责加载Tomcat使用的类和Jar包以及应用通用的一些类和Jar包,例如CATALINA_HOME/lib目录下的所有类和Jar包。

Tomcat会为每个部署的应用创建一个唯一的类加载器,也就是WebApp ClassLoader,它负责加载该应用的WEB-INF/lib目录下的Jar文件以及WEB-INF/classes目录下的Class文件。「由于每个应用都有自己的WebApp ClassLoader,这样就可以使不同的Web应用之间相互隔离,彼此之间看不到对方使用的类文件。即使不同项目下的类全限定名有可能相等,也能正常工作」。


而对应用进行热部署时,会抛弃原有的WebApp ClassLoader,并为应用创建新的WebApp ClassLoader。


本文小结

本文详细介绍了JVM中类加载,以及双亲委派模型与双亲模型被破坏相关的知识与内容。

为什么要破坏JVM的双亲委派模型相关推荐

  1. 模块化加载_谈谈双亲委派模型的第四次破坏-模块化

    前言 JDK9引入了Java模块化系统(Java Platform Moudle System)来实现可配置的封装隔离机制,同时JVM对类加载的架构也做出了调整,也就是双亲委派模型的第四次破坏.前三次 ...

  2. 谈谈双亲委派模型的第四次破坏-模块化

    " JDK9引入了模块化系统来实现可配置的封装隔离机制,同时JVM对类加载的架构也做出了调整,也就是双亲委派模型的第四次破坏" 01 双亲委派模型 简介 在JDK9引入之前,绝大多 ...

  3. JVM原理系列--双亲委派模型

    原文网址:JVM原理系列--双亲委派模型_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java虚拟机的双亲委派模型. 工作过程 说明 双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求, ...

  4. amba simple class驱动_学习笔记:class加载器和双亲委派模型

    类加载器 类加载器有四种 启动类加载器(Bootstrap ClassLoader) 负责加载 JAVA_HOMElib ⽬录中的,或通过-Xbootclasspath参数指定路径中的且被虚拟机认可( ...

  5. 类加载器、双亲委派模型

    目录 1.简介 2.类和类加载器 3.双亲委派模型 3.1 启动类加载器: 3.2 扩展类加载器 3.3应用程序类加载器 3.4  类加载器的双亲委派模型(Parents Delegation Mod ...

  6. 双亲委派模型和破坏性双亲委派模型详解

    从JVM的角度来看,只存在两种类加载器: 启动类加载器(Bootstrap ClassLoader):由C++语言实现(针对HotSpot),负责将存放在<JAVA_HOME>\lib目录 ...

  7. 双亲委派模型以及SpringFactoriesLoader详解(最全最简单的介绍)

    文章目录 前言 类加载的过程 类加载器 何为双亲委派模型 ClassLoader类的loadClass方法 双亲委派模型存在的问题 解决办法 以JDBC驱动管理为例 加载资源 SpringFactor ...

  8. JVM 破坏双亲委派模型

    双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式.在 Java的世界中大部分的类加载器都遵循这个模型,但也有例外的情况,直到Java模块化出现为止,双亲委 ...

  9. 【深入理解JVM】:类加载器与双亲委派模型

    转载自  [深入理解JVM]:类加载器与双亲委派模型 类加载器 加载类的开放性 类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因.在类加载的第一阶段" ...

最新文章

  1. 推出第一个免费工具CCT
  2. Web站点下的Web Service读取此Web站点中的Session值
  3. 如何解决GBK的编码的文件中的中文转换成为UTF-8编码的文件而且不乱码
  4. ReultSet有什么作用和使用
  5. emu8086汇编——字符串匹配算法程序
  6. 使用Unity3D视频转换器TheoraConverter.NET 1.1 Setup转换视频格式为ogv并播放视频
  7. 非系统盘根目录出现Msdia80.dll文件如何处理
  8. 前端vue后台管理系统项目优化
  9. 高等数学:第一章 函数与极限(6)极限存在准则、两个重要极限
  10. html文本打印lt;igt;字段,6-HTMLlt; formgt;表单标签和属性
  11. 第56章 SQL UCASE() 函数教程
  12. MATLAB | 一文解决各类曲面交线绘制,包含三维隐函数曲面交线
  13. 工程师的基本功是什么?该如何练习?
  14. 8086的两种工作模式_8086有哪两种工作模式?其主要区别是什么?
  15. Delphi Assigned 简单使用
  16. Android OpenCV实现文字识别
  17. 美团、大众点评 token最新算法——宇宙第一简洁版
  18. Jxta 命令 shell
  19. 余弦相似性:找出相似文章
  20. linux 网卡绑定team和删除team

热门文章

  1. Windows server 2012 安装exchange 2013
  2. 【编程好习惯】复用代码以提高可维护性
  3. 下拉式菜单在GridView编辑时联动选择
  4. XP下安装SQL2000企业版
  5. NuGet程序包安装SQLite后完全抽离出SQLite之入门介绍及注意事项,你真的懂了吗?...
  6. C#如何解决对ListView控件更新以及更新时界面闪烁问题
  7. SetDockingMode 设置dock停泊方式
  8. 接到骗子短信后........
  9. Octavia API接口慢问题排查引发的思考
  10. Nginx的client_header_buffer_size和large_client_header_buffers学习