文章目录

  • 一、前言
  • 二、类加载器
  • 三、双亲委派机制
    • 1、什么是双亲委派
    • 2、为什么要双亲委派?
  • 四、破坏双亲委派
    • 1、直接自定义类加载器加载
    • 2、跳过AppClassLoader和ExtClassLoader
    • 3、自定义类加载器加载扩展类
    • 4、Tomcat中破坏双亲委派的场景
    • 5、一个比较完整的自定义类加载器
  • 五、Class.forName和ClassLoader.loadClass区别
  • 六、线程上下文类加载器
  • 七、要点回顾

一、前言

平时做业务开发比较少接触类加载器,但是如果想深入学习Tomcat、Spring等开源项目,或者从事底层架构的开发,了解甚至熟悉类加载的原理是必不可少的。

java的类加载器有哪些?什么是双亲委派?为什么要双亲委派?如何打破它?多多少少对这些概念了解一些,甚至因为应付面试背过这些知识点,但是再深入一些细节,却知之甚少。

二、类加载器

类加载器,顾名思义就是一个可以将Java字节码加载为java.lang.Class实例的工具。这个过程包括,读取字节数组、验证、解析、初始化等。另外,它也可以加载资源,包括图像文件和配置文件。

类加载器的特点:

  • 动态加载,无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包jar、war中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。
  • 全盘负责,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。

一个类的唯一性由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的equals()isAssignableFrom()isInstance()以及instanceof关键字等),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。

从实现方式上,类加载器可以分为两种:一种是启动类加载器,由C++语言实现,是虚拟机自身的一部分;另一种是继承于java.lang.ClassLoader的类加载器,包括扩展类加载器应用程序类加载器以及自定义类加载器。

启动类加载器Bootstrap ClassLoader):负责加载<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果想设置Bootstrap ClassLoader为其parent可直接设置null

扩展类加载器Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定路径中的所有类库。该类加载器由sun.misc.Launcher$ExtClassLoader实现。扩展类加载器由启动类加载器加载,其父类加载器为启动类加载器,即parent=null

应用程序类加载器Application ClassLoader):负责加载用户类路径(ClassPath)上所指定的类库,由sun.misc.Launcher$App-ClassLoader实现。开发者可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器,所以也可称它为系统类加载器。应用程序类加载器也是启动类加载器加载的,但是它的父类加载器是扩展类加载器。在一个应用程序中,系统类加载器一般是默认类加载器。

三、双亲委派机制

1、什么是双亲委派

JVM 并不是在启动时就把所有的.class文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。除了启动类加载器外,其他所有类加载器都需要继承抽象类ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。

public abstract class ClassLoader {//每个类加载器都有个父加载器private final ClassLoader parent;public Class<?> loadClass(String name) {//查找一下这个类是不是已经加载过了Class<?> c = findLoadedClass(name);//如果没有加载过if( c == null ){//先委派给父加载器去加载,注意这是个递归调用if (parent != null) {c = parent.loadClass(name);}else {// 如果父加载器为空,查找Bootstrap加载器是不是加载过了c = findBootstrapClassOrNull(name);}}// 如果父加载器没加载成功,调用自己的findClass去加载if (c == null) {c = findClass(name);}return c;}protected Class<?> findClass(String name){//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存...//2. 调用defineClass将字节数组转成Class对象return defineClass(buf, off, len);}// 将字节码数组解析成一个Class对象,用native方法实现protected final Class<?> defineClass(byte[] b, int off, int len){...}
}

从上面的代码可以得到几个关键信息:

  • JVM 的类加载器是分层次的,它们有父子关系,而这个关系不是继承维护,而是组合,每个类加载器都持有一个 parent 字段,指向父加载器。(AppClassLoaderparentExtClassLoaderExtClassLoaderparentBootstrapClassLoader,但是ExtClassLoaderparent=null。)
  • defineClass 方法的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象。
  • findClass 方法的主要职责就是找到.class文件并把.class文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。子类必须实现findClass
  • loadClass 方法的主要职责就是实现双亲委派机制:首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载

2、为什么要双亲委派?

双亲委派保证类加载器,自下而上的委派,又自上而下的加载,保证每一个类在各个类加载器中都是同一个类。

一个非常明显的目的就是保证java官方的类库<JAVA_HOME>\lib和扩展类库<JAVA_HOME>\lib\ext的加载安全性,不会被开发者覆盖。

例如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。

如果开发者自己开发开源框架,也可以自定义类加载器,利用双亲委派模型,保护自己框架需要加载的类不被应用程序覆盖。

四、破坏双亲委派

双亲委派模型并不是一个具有强制性约束的模型,而是Java设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。

如果想自定义类加载器,就需要继承ClassLoader,并重写findClass,如果想不遵循双亲委派的类加载顺序,还需要重写loadClass

1、直接自定义类加载器加载

如下是一个自定义的类加载器TestClassLoader,并重写了findClassloadClass

public class TestClassLoader extends ClassLoader {public TestClassLoader(ClassLoader parent) {super(parent);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 1、获取class文件二进制字节数组byte[] data = null;try {System.out.println(name);String namePath = name.replaceAll("\\.", "\\\\");String classFile = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\" + namePath + ".class";ByteArrayOutputStream baos = new ByteArrayOutputStream();FileInputStream fis = new FileInputStream(new File(classFile));byte[] bytes = new byte[1024];int len = 0;while ((len = fis.read(bytes)) != -1) {baos.write(bytes, 0, len);}data = baos.toByteArray();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}// 2、字节码加载到 JVM 的方法区,// 并在 JVM 的堆区建立一个java.lang.Class对象的实例// 用来封装 Java 类相关的数据和方法return this.defineClass(name, data, 0, data.length);}@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException{Class<?> clazz = null;// 直接自己加载clazz = this.findClass(name);if (clazz != null) {return clazz;}// 自己加载不了,再调用父类loadClass,保持双亲委托模式return super.loadClass(name);}
}

测试:
初始化自定义的类加载器,需要传入一个parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:

public static void main(String[] args) throws Exception {// 初始化TestClassLoader,被将加载TestClassLoader类的类加载器设置为TestClassLoader的parentTestClassLoader testClassLoader = new TestClassLoader(TestClassLoader.class.getClassLoader());System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());// 加载 DemoClass clazz = testClassLoader.loadClass("study.stefan.classLoader.Demo");System.out.println("Demo的类加载器:" + clazz.getClassLoader());}

运行如下测试代码,发现报错了:
找不到java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?

转瞬想到java中所有的类都隐含继承了超类Object,加载study.stefan.classLoader.Demo,也会加载父类ObjectObjectstudy.stefan.classLoader.Demo并不在同个目录,那就找到Object.class的目录(将jre/lib/rt.jar解压),修改TestClassLoader#findClass如下:
遇到前缀为java.的就去找官方的class文件。

运行测试代码:
还是报错了!!!

报错信息为:Prohibited package name: java.lang
跟了下异常堆栈:
TestClassLoader#findClass最后一行代码调用了java.lang.ClassLoader#defineClass
java.lang.ClassLoader#defineClass最终调用了如下代码:


看意思是java禁止用户用自定义的类加载器加载java.开头的官方类,也就是说只有启动类加载器BootstrapClassLoader才能加载java.开头的官方类。

得出结论,因为java中所有类都继承了Object,而加载自定义类study.stefan.classLoader.Demo,之后还会加载其父类,而最顶级的父类Object是java官方的类,只能由BootstrapClassLoader加载。

2、跳过AppClassLoader和ExtClassLoader

既然如此,先将study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可。
由于java中无法直接引用BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader

package com.stefan.DailyTest.classLoader;public class Test {public static void main(String[] args) throws Exception {// 初始化TestClassLoader,并将加载TestClassLoader类的类加载器// 设置为TestClassLoader的parentTestClassLoader testClassLoader = new TestClassLoader(null);System.out.println("TestClassLoader的父类加载器:" + testClassLoader.getParent());// 加载 DemoClass clazz = testClassLoader.loadClass("com.stefan.DailyTest.classLoader.Demo");System.out.println("Demo的类加载器:" + clazz.getClassLoader());}
}

双亲委派的逻辑在 loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass

运行测试代码:

成功了,Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。

如果不破坏双亲委派,那么Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派。

3、自定义类加载器加载扩展类

假设classpath下由上述TestClassLoader加载的类中用到了<JAVA_HOME>\lib\ext下的扩展类,那么这些扩展类也会由TestClassLoader加载,但是会报类文件找不到的情况。
但是自定义类加载器也是能加载<JAVA_HOME>\lib\ext下的扩展类的,只要自定义类加载器能找准扩展类的类路径。

以扩展目录com.sun.crypto.provider下的类举例:
(1)Demo中随便引用一个扩展类:

import com.sun.crypto.provider.ARCFOURCipher;
public class Demo {public Demo() {ARCFOURCipher arcfourCipher = new ARCFOURCipher();System.out.println("ARCFOURCipher.getClassLoader=" + arcfourCipher.getClass().getClassLoader());}
}

(2)修改TestClassLoader#findClass:

(3)测试代码中需要调用一下Demo类的构造器:

(4)运行测试代码
自定义类加载器成功加载了扩展类。

由上得出结论,<JAVA_HOME>\lib\ext下的扩展类是没有强制只有ExtClassLoader能加载,自定义类加载器也能加载。

4、Tomcat中破坏双亲委派的场景

只有官方库java.的类必须由启动类加载器加载,无法破坏,扩展类加载器和应用程序类加载器的双亲委派都是可以破坏的。

知道了理论,还需要根据实际场景,找准破坏双亲委派的位置。可以看看优秀的开源框架中是如何破坏双亲委派的,比如Tomcat:

Tomcat源码就不贴了,Tomcat中可以部署多个web项目,为了保证每个web项目互相独立,所以不能都由AppClassLoader加载,所以自定义了类加载器WebappClassLoaderWebappClassLoader继承自URLClassLoader,重写了findClassloadClass,并且WebappClassLoader的父类加载器设置为AppClassLoader
WebappClassLoader.loadClass中会先在缓存中查看类是否加载过,没有加载,就交给ExtClassLoaderExtClassLoader再交给BootstrapClassLoader加载;都加载不了,才自己加载;自己也加载不了,就遵循原始的双亲委派,交由AppClassLoader递归加载。

5、一个比较完整的自定义类加载器

一般情况下,自定义类加载器都是继承URLClassLoader,具有如下类关系图:

public class TestClassLoader extends URLClassLoader {public TestClassLoader(ClassLoader parent) {super(new URL[0], parent);}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 1、先自己的路径找Class<?> clazz = null;try {clazz = findClassInternal(name);} catch (Exception e) {// Ignore}if (clazz != null) {return clazz;}// 在 父类路径 找return super.findClass(name);}private Class<?> findClassInternal(String name) throws IOException {byte[] data = null;try {String dir = "C:\\study\\myStudy\\ZooKeeperLearning\\zkops\\target\\classes\\";String namePath = name.replaceAll("\\.", "\\\\");String classFile = dir + namePath + ".class";ByteArrayOutputStream baos = new ByteArrayOutputStream();FileInputStream fis = new FileInputStream(new File(classFile));byte[] bytes = new byte[1024];int len = 0;while ((len = fis.read(bytes)) != -1) {baos.write(bytes, 0, len);}data = baos.toByteArray();// 字节码加载到 JVM 的方法区,// 并在 JVM 的堆区建立一个java.lang.Class对象的实例// 用来封装 Java 类相关的数据和方法return this.defineClass(name, data, 0, data.length);} catch (Exception e) {throw e;}}@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException{// 1、先委托给ext classLoader 加载ClassLoader classLoader = getSystemClassLoader();while (classLoader.getParent() != null) {classLoader = classLoader.getParent();}Class<?> clazz = null;try {clazz = classLoader.loadClass(name);} catch (ClassNotFoundException e) {// Ignore}if (clazz != null) {return clazz;}// 2、自己加载clazz = this.findClass(name);if (clazz != null) {return clazz;}// 3、自己加载不了,再调用父类loadClass,保持双亲委托模式return super.loadClass(name);}
}

五、Class.forName和ClassLoader.loadClass区别

  1. forName(String name, boolean initialize,ClassLoader loader) 可以指定classLoader
  2. 不显式传classLoader就是默认当前类的类加载器:
public static Class<?> forName(String className)throws ClassNotFoundException {Class<?> caller = Reflection.getCallerClass();return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

类加载过程:加载——》验证——》准备——》解析——》类初始化——》使用(对象实例初始化)——》卸载

java.lang.Class.forName 会调用到forName0方法,第二个参数 initialize = true,意为会进行类初始化(<clinit>())操作。

java.lang.ClassLoader.loadClass 会调用到 protected 修饰的 loadClass(String name, boolean resolve),第2个参数resolve=false,意为不进行类的解析操作,也就不会进行类初始化,包括静态变量的初始化、静态代码块的运行,都不会进行。

六、线程上下文类加载器

线程上下文类加载器其实是一种类加载器传递机制。可以通过java.lang.Thread#setContextClassLoader方法给一个线程设置上下文类加载器,在该线程后续执行过程中就能把这个类加载器取(java.lang.Thread#getContextClassLoader)出来使用。

如果创建线程时未设置上下文类加载器,将会从父线程(parent = currentThread())中获取,如果在应用程序的全局范围内都没有设置过,就默认是应用程序类加载器。

线程上下文类加载器的出现就是为了方便破坏双亲委派:

一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能去加载ClassPath下的类。

但是有了线程上下文类加载器就好办了,JNDI服务使用线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。

Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

摘自《深入理解java虚拟机》周志明

七、要点回顾

  1. java 的类加载,就是获取.class文件的二进制字节码数组并加载到 JVM 的方法区,并在 JVM 的堆区建立一个用来封装 java 类相关的数据和方法的java.lang.Class对象实例。
  2. java默认有的类加载器有三个,启动类加载器(BootstrapClassLoader),扩展类加载器(ExtClassLoader),应用程序类加载器(也叫系统类加载器)(AppClassLoader)。类加载器之间存在父子关系,这种关系不是继承关系,是组合关系。如果parent=null,则它的父级就是启动类加载器。启动类加载器无法被java程序直接引用。
  3. 双亲委派就是类加载器之间的层级关系,加载类的过程是一个递归调用的过程,首先一层一层向上委托父类加载器加载,直到到达最顶层启动类加载器,启动类加载器无法加载时,再一层一层向下委托给子类加载器加载。
  4. 加载一个类时,也会加载其父类,如果该类中还引用了其他类,则按需加载,且类加载器都是加载当前类的类加载器。
  5. 双亲委派的目的主要是为了保证java官方的类库<JAVA_HOME>\lib加载安全性,不会被开发者覆盖。
  6. <JAVA_HOME>\lib<JAVA_HOME>\lib\ext是java官方核心类库,一般不会去破坏ExtClassLoader及其以上的双亲委派。
  7. 破坏双亲委派有两种方式:第一种,自定义类加载器,必须重写findClassloadClass;第二种是通过线程上下文类加载器的传递性,让父类加载器中调用子类加载器的加载动作。
  8. ClassLoader.loadClassClass.forName 区别在于,ClassLoader.loadClass 不会对类进行解析和类初始化,而 Class.forName 是有正常的类加载过程的。

参考:

  • 《深入理解java虚拟机》周志明(书中对类加载的介绍非常详尽,部分精简整理后引用。)
  • 《深入拆解Tomcat & Jetty》Tomcat如何打破双亲委托机制?李号双
  • 《Tomcat内核设计剖析》汪建,第十三章 公共与隔离的类加载器

Java双亲委派模型:为什么要双亲委派?如何打破它?破在哪里?相关推荐

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

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

  2. java类加载和双亲委派模型浅说

    本文目录 前言 一.类加载器 1.1 类加载机制的基本特征 1.2 类加载的分类 1.3 类加载器 A.启动类加载器(引导类加载器,Bootstrap ClassLoader) B.扩展类加载器(Ex ...

  3. java 类加载 双亲委派_java类加载器和双亲委派模型

    一. 类加载器 ClassLoader即常说的类加载器,其功能是用于从Class文件加载所需的类,主要场景用于热部署.代码热替换等场景. 系统提供3种的类加载器:Bootstrap ClassLoad ...

  4. java破坏双亲委派_破坏双亲委派模型

    上次说了类加载器以及它的双亲委派模型,同样提到了双亲委派模型并不是一种强制的约束,而是推荐给开发者的类加载器的实现方式,在java中,大部分类加载器都会遵循这个模型,但是也有例外,到目前为止,双亲委派 ...

  5. 深入理解什么是双亲委派模型(Java图文详解)

    [辰兮要努力]:hello你好我是辰兮,很高兴你能来阅读,昵称是希望自己能不断精进,向着优秀程序员前行! 博客来源于项目以及编程中遇到的问题总结,偶尔会有读书分享,我会陆续更新Java前端.后台.数据 ...

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

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

  7. Tomcat类加载器为何违背双亲委派模型

    本文来说下Tomcat类加载器为何违背双亲委派模型 文章目录 什么是类加载机制 什么是双亲委派模型 如何破坏双亲委任模型 Tomcat的类加载器是怎么设计的 本文小结 什么是类加载机制 代码编译的结果 ...

  8. 什么情况下需要破坏双亲委派模型

    双亲委派模型的破坏 双亲委派模型的第一次"被破坏"其实发生在双亲委派模型出现之前–即JDK1.2发布之前.由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java ...

  9. 什么是双亲委派模型?

    分析&回答 双亲委派模型 原理:当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成 ...

  10. java 打破双亲委派,为什么说java spi破坏双亲委派模型?

    虽然有SPI破坏双亲委派模型的说法,但我不太认同.简单说下. 双亲委派模型(再次吐槽下这个翻译),是一种加载类的约定.这个约定的一个用处是保证安全.比如说你写Java用了String类,你怎么保证你用 ...

最新文章

  1. 挑灯熬夜看《Build 2015 Keynote》图文笔记
  2. 试读angular源码第三章:初始化zone
  3. python while循环语句-Python
  4. 产品经理的方向感-产品生命周期
  5. 一篇文章告诉你[C++]数组初始化
  6. 联想Z5 Pro划时代旗舰发布,屏占比95.06%售价1998元起
  7. 《疯狂Java讲义》(十八)---- JAR文件
  8. 【云周刊】第148期:“盲人摸象、感而不动、雾里看花”,阿里闵万里谈城市大脑三大挑战...
  9. java 开源 dht_P2P中DHT网络原理
  10. IAST安全扫描原理
  11. python+keras实现语音识别
  12. QQ等级图标对应的算法
  13. 每周读书#12 - 秘密
  14. MicroPython ESP32 ADC(模拟量转数字量)示例
  15. 初识html5使用jsQR识别二维码
  16. 此beta版已额满_坚果 Pro 3 发布 Smartisan OS v7.5.0早期众测版
  17. work english words
  18. C和C++中的struct
  19. mysql 时间戳转换为时间_将MYSQL数据库里的时间戳转换成时间
  20. 【技术文档】麦肯锡“七步成诗”之Bug管理系统设计

热门文章

  1. java倒计时跳出窗口_java 窗口 倒计时 关闭
  2. 蜂鸟E203 SOC开源资料汇总 及 RISC-V基础
  3. Tampermonkey的安装和使用
  4. 融云发送图片消息_基于融云的IM通讯
  5. 在线CAD的妙用大揭秘,还不快看过来!
  6. Warning: [antd: Switch] `value` is not a valid prop, do you mean `checked`?
  7. OSI七层模型:会话层、表示层、应用层
  8. C语言学生学籍管理系统源程序|用数据文件存放学生的学籍,可对学生学籍进行注册,登录,修改,删除,查找,统计,学籍变化等操作。(用文件保存) 功能要求: (1) 系统以菜单方式工作。 (2) 登记学生的
  9. 滚动条插件vue-scroll
  10. SQL数据库对字段的操作(alter table)