类加载双亲委派

双亲委派机制是Java中非常重要的类加载机制,它保证了类加载的完整性和安全性,避免了类的重复加载。
们的Java在运行之前,首先需要把Java代码转换成字节码,即class文件。
然后JVM需要把字节码通过一定的方式加载到内存中的运行时数据区
这种方式就是类加载器(ClassLoader)。
再通过加载、验证、准备、解析、初始化这几个步骤完成类加载过程,然后再由jvm执行引擎的解释器和JIT即时编译器去将字节码指令转换为本地机器指令进行执行。
我们在使用类加载器加载类的时候,会面临下面几个问题:
  1. 如何保证类不会被重复加载?类重复加载会出现很多问题。
  1. 类加载器是否允许用户自定义?
  1. 如果允许用户自定义,如何保证类文件的安全性?
  1. 如何保证加载的类的完整性?
双亲委派机制的基本思想是:当一个类加载器试图加载某个类时,它会先委托给其父类加载器,如果父类加载器无法加载,再由当前类加载器自己进行加载。
这种层层委派的方式有助于保障类的唯一性,避免类的重复加载,并提高系统的安全性和稳定性。

双亲委派

notion image
notion image

案例

case1:自定义一个java.lang.String(与Java的核心API String是冲突的)类,加载时会从下面应用程序类加载器始向上走,每一个加载器查看自己是否加载过,如果没有加载过就继续向上,最后到启动类加载器(Bootstrap ClassLoader),启动类加载器发现自己已经加载过String类了,所以就不会再加载了,然后String加载就结束了(启动类加载器Bootstrap ClassLoader会加载Java的核心库JAVAHOME/jre/1ib/rt.jar、resources.jar或sun.boot.class.path路径下的内容,用于提供JVM自身需要的类,String类就属于这里面的一种;怎样判断是否加载过:类的完成类名必须一致,包括包名、加载这个类的 ClassLoader(指ClassLoader实例对象)必须相同); case2:比如我又定义一个类,经过应用程序类加载器,发现自己没被加载过而且应用程序加载器还有上层扩展类加载器,那么就会到扩展类加载器,扩展类加载器继续来判断自己是否加载过,如果发现自己没被加载过而且扩展类加载器还有上层启动类加载器,那么就会到启动类加载器,最后到启动类加载器后自己也没有加载过,然后开始尝试自己加载,加载成功就结束,加载失败,就退回扩展类加载器加载(这里加载成功或是失败是看自己的这个加载器是否有加载这个类的责任,每一个加载器都有一定的责任范围,在下面虚拟机自带的加载器介绍中可以看到加载器的可以加载哪些类),扩展类尝试加载,成功就结束,失败了就交给应用程序类加载器去加载,如果应用类加载器可以加载就加载成功结束,如果不可以加载就抛出异常;

原理

如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是看看自己是否加载过(如果加载过结束),然后看看有没有父类(如果有交给父类加载器执行); 如果父类加载器自己没有加载过还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器; 然后顶层加载器发现这个类还是没有被加载过,然后就会去尝试加载,加载成功就结束了,加载失败就退回到子类,让子类尝试去加载,这就是双亲委派模式。 父类加载器一层一层往下分配任务,如果子类加载器能加载,则加载此类,如果将加载任务分配至系统类加载器也无法加载此类,则抛出异常

优势

  • <mark>避免类的重复加载</mark>,JVM 中区分不同类,不仅仅是根据类名,相同的 class 文件被不同的 ClassLoader 加载就属于两个不同的类(比如,Java中的Object类,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,如果不采用双亲委派模型,由各个类加载器自己去加载的话,系统中会存在多种不同的 Object 类)
  • <mark>保护程序安全,防止核心 API 被随意篡改</mark>,避免用户自己编写的类动态替换 Java 的一些核心类,比如我们自定义类:java.lang.String(已经在上面举例了)
在 JVM 中表示两个 class 对象是否为同一个类存在两个必要条件:
  • 类的完成类名必须一致,包括包名
  • 加载这个类的 ClassLoader(指ClassLoader实例对象)必须相同

虚拟机自带的加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部
  • 它用来加载Java的核心库(JAVAHOME/jre/1ib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自ava.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

应用程序类加载器(系统类加载器,AppClassLoader)

  • javI语言编写,由sun.misc.LaunchersAppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载
  • 通过classLoader#getSystemclassLoader()方法可以获取到该类加载器

用户自定义类加载器

在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,来定制类的加载方式。 为什么要自定义类加载器?
  • 隔离加载类——不同的中间件都会去自实现自定义类加载器
  • 修改类加载的方式——需要的时候动态加载
  • 扩展加载源——比如从数据库中、机顶盒中
  • 防止源码泄漏——对字节码加密,运行的时候去解密

流程

notion image
具体流程大概是这样的:
  1. 需要加载某个类时,先检查自定义类加载器是否加载过,如果已经加载过,则直接返回。
  1. 如果自定义类加载器没有加载过,则检查应用程序类加载器是否加载过,如果已经加载过,则直接返回。
  1. 如果应用程序类加载器没有加载过,则检查扩展类加载器是否加载过,如果已经加载过,则直接返回。
  1. 如果扩展类加载器没有加载过,则检查启动类加载器是否加载过,如果已经加载过,则直接返回。
  1. 如果启动类加载器没有加载过,则判断当前类加载器能否加载这个类,如果能加载,则加载该类,然后返回。
  1. 如果启动类加载器不能加载该类,则交给扩展类加载器。扩展类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。
  1. 如果扩展类加载器不能加载该类,则交给应用程序类加载器。应用程序类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。
  1. 如果应用程序类加载器不能加载该类,则交给自定义类加载器。自定义类加载器判断能否加载这个类,如果能加载,则加载该类,然后返回。
  1. 如果自定义类加载器,也无法加载这个类,则直接抛ClassNotFoundException异常。
这样做的好处是:
  1. 保证类不会重复加载。加载类的过程中,会向上问一下是否加载过,如果已经加载了,则不会再加载,这样可以保证一个类只会被加载一次。
  1. 保证类的安全性。核心的类已经被启动类加载器加载了,后面即使有人篡改了该类,也不会再加载了,防止了一些有危害的代码的植入。

打破双亲委派场景

  1. JNDI服务:Java命名和目录接口(JNDI)需要调用独立厂商实现的代码,这些代码位于应用程序的ClassPath下,而启动类加载器无法加载这些外部代码。为了解决这个问题,Java引入了线程上下文类加载器(Thread Context ClassLoader),允许父类加载器请求子类加载器完成类加载,从而打破了双亲委派机制。
  1. JDBC驱动加载:JDBC的Driver接口的具体实现通常由不同的数据库厂商提供,并且位于应用程序的ClassPath下。由于启动类加载器不能加载用户编写的代码,因此需要应用程序类加载器来加载这些Driver实现类,这也破坏了双亲委派机制。
  1. Tomcat容器:Tomcat作为一个Servlet容器,需要同时加载Servlet相关的jar包和Tomcat自身的类。由于Tomcat可以部署多个Web应用,且每个应用可能需要加载不同的依赖版本,传统的双亲委派机制无法满足这种需求。Tomcat采用了一种特殊的类加载机制,包括CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader,以实现类加载的隔离和共享。
  1. 热部署和OSGi:为了实现代码的热部署和热替换,引入了OSGi框架。OSGi中的每个模块(Bundle)都有自己的类加载器,这些加载器之间是平级关系,没有固定的委派关系,从而允许模块独立更新和加载,这也破坏了双亲委派机制
Loading...
目录
文章列表
王小扬博客
产品
Think
Git
软件开发
计算机网络
CI
DB
设计
缓存
Docker
Node
操作系统
Java
大前端
Nestjs
其他
PHP