type
status
date
slug
summary
tags
category
password
一、类加载器
1、类加载器是什么
类加载器的作用是负责类加载过程的工具,即负责将 Java 字节码文件(
.class 文件)加载到内存,经过数据验证、解析、初始化等步骤,最后转换成可以被 JVM 执行的方法区的运行时数据结构(java.lang.Class 对象)。类加载器的特点:
- 动态加载,无需在程序一开始运行的时候加载,而是在程序运行的过程中,动态按需加载,字节码的来源也很多,压缩包 jar、war中,网络中,本地文件等。类加载器动态加载的特点为热部署,热加载做了有力支持。
- 全盘负责,当一个类加载器加载一个类时,这个类所依赖的、引用的其他所有类都由这个类加载器加载,除非在程序中显式地指定另外一个类加载器加载。所以破坏双亲委派不能破坏扩展类加载器以上的顺序。
- 类加载器只负责加载
.class文件,至于是否可以运行,则由 Execution Engine 决定。
- 加载的类信息存放在方法区里面,每个类都会有指向类加载器的引用,作为类型信息的一部分一起保存在方法区中。

2、类加载器的分类
从实现方式上,类加载器可以分为两种:一种是启动类加载器,由C++语言实现,是虚拟机自身的一部分;另一种是继承于
java.lang.ClassLoader的类加载器,包括扩展类加载器、应用程序类加载器以及自定义类加载器。
类加载器 | 作用 | 说明 |
启动类加载器(Bootstrap ClassLoader) | 使用 C/C++ 实现,嵌在 JVM 内部,负责 JVM 自身需要的核心类库。从下面路径加载类库:
1、 <JAVA_HOME>/jre/lib
2、-Xbootclasspath参数所指定的路径
出于安全考虑,虚拟机只加载特定的 jar 文件(按照文件名识别,如 rt.jar),只加载包名为 java、javax、sun 等开头的类到虚拟机内存中。名字不符合的类库即使放在 lib目录中也不会被加载。 | 1、并不继承自 java.lang.ClassLoader ,没有父加载器。
2、启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果想设置Bootstrap ClassLoader为其parent,可直接设置 null。 |
扩展类加载器(Extension ClassLoader) | 负责加载 Java 的扩展类库。从下面路径加载类库:
1、 <JAVA_HOME>/jre/lib/ext
2、java.ext.dirs系统变量所指定路径
用户创建的 jar 放在这些目录下也会被自动加载。
| 1、由 sun.misc.Launcher$ExtClassLoader实现
2、由 Bootstrap 加载器加载,其父加载器为 Bootstrap 加载器,即parent=null。
|
应用程序类加载器(Application ClassLoader) | 也称为系统类加载器,用户程序中的默认的类加载器,负责加载开发者编写的类。从下面路经加载类库
1、环境变量 CLASSPATH 下的类库。
2、系统属性 java.class.path 下的类库。 | 1、由 sun.misc.Launcher$AppClassLoader实现
2、由 Bootstrap 加载器加载,但是它的父类加载器是扩展类加载器。
3、可直接通过java.lang.ClassLoader中的getSystemClassLoader()方法获取应用程序类加载器 |
用户自定义类加载器 | 用户可以通过继承 java.lang.ClassLoader 的方式来实现自己的类加载器。 | 1、不建议用户覆盖 loadClass() 方法,因为有可能打破双亲委派机制。建议只覆盖 findClass() 方法定义自己的类加载逻辑。
2、用户如果没有太复杂的需求,可以直接继承 URLClassLoader 类,这样用户也不需要覆盖 findClass() 方法重写获取字节码流的方式,使自己的类加载器更简洁。 |
除了启动类加载器外,其他所有类加载器都需要继承抽象类
java.lang.ClassLoader
这个抽象类中定义了三个关键方法:
findClass(String name):主要职责就是找到.class文件并把.class文件读到内存得到字节码数组,然后调用defineClass方法把字节码转换成 Class 对象。子类必须重写findClass。
defineClass(String name, byte[] b, int off, int len):调用 native 方法将字节码数组解析成一个 Class 对象。
loadClass(String name):实现双亲委派机制的核心,首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则委派给父加载器加载,这是一个递归调用,一层一层向上委派,最顶层的类加载器(启动类加载器)无法加载该类时,再一层一层向下委派给子类加载器加载。
从上面的代码可以得到几个信息:
- JVM 的类加载器是分层次的,它们有父子关系,但这个关系不是继承关系,而是组合关系。每个类加载器都持有一个
parent字段,指向父加载器。(AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是BootstrapClassLoader,但是ExtClassLoader的parent = null)。
- 因为
loadClass()是实现双亲委派模型的关键,所以不建议用户覆盖loadClass()方法,因为有可能打破双亲委派机制。建议只覆盖findClass()方法定义查找类的逻辑。
二、双亲委派机制是什么
双亲委派机制是指:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。

为什么需要双亲委派机制?
- 保证 Java 核心 API 的安全性:防止核心 API 被开发者篡改,保证
java官方的类库<JAVA_HOME>/jre/lib和扩展类库<JAVA_HOME>/jre/lib/ext的加载安全性。例如用户自定义了一个java.lang.String类,编译的时候不会报错,但是该类不会被加载,因为 Bootstrap 加载器已经成功加载了 java 核心类库的java.lang.String类,所以在执行的时候会报错。

- 避免类的重复加载:保证一个类在各个类加载器中都是同一个类。例如
java.lang.Object类存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。
三、破坏双亲委派
双亲委派模型并不是一个具有强制性约束的模型,而是 Java 设计者推荐给开发者们的类加载器实现方式。这个委派和加载顺序完全是可以被破坏的。
只有官方库
java.的类必须由启动类加载器加载,无法破坏,扩展类加载器和应用程序类加载器的双亲委派都是可以破坏的。破坏双亲委派机制需要自定义类加载器继承ClassLoader,并重写findClass 和 loadClass 方法。如果继承 ClassLoader 类,findClass方法是必须要重写的;loadClass 方法是用来保证双亲委派机制的,所以破坏双亲委派的关键步骤是重写 loadClass 方法。1、自定义类加载器
1.1 自定义类加载器跳过AppClassLoader和ExtClassLoader
如下是一个自定义的类加载器
TestClassLoader,并重写了findClass和loadClass:开始测试,初始化自定义的类加载器,需要传入一个
parent,指定其父类加载器,那就先指定为加载TestClassLoader的类加载器为TestClassLoader的父类加载器吧:运行如下测试代码,发现报错了:
找不到
java\lang\Object.class,我加载study.stefan.classLoader.Demo类和Object有什么关系呢?
转瞬想到java中所有的类都隐含继承了超类
Object,加载study.stefan.classLoader.Demo,也会加载父类Object。Object和study.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加载。既然如此,先将
study.stefan.classLoader.Demo交由BootstrapClassLoader加载即可。由于java中无法直接引用
BootstrapClassLoader,所以在初始化TestClassLoader时,传入parent为null,也就是TestClassLoader的父类加载器设置为BootstrapClassLoader:双亲委派的逻辑在
loadClass,由于现在的类加载器的关系为TestClassLoader —>BootstrapClassLoader,所以TestClassLoader中无需重写loadClass。运行测试代码:

成功了,
Demo类由自定义的类加载器TestClassLoader加载的,双亲委派模型被破坏了。如果不破坏双亲委派,那么
Demo类处于classpath下,就应该是AppClassLoader加载的,所以真正破坏的是AppClassLoader这一层的双亲委派。1.2 自定义类加载器加载扩展类
假设
classpath下由上述TestClassLoader加载的类中用到了<JAVA_HOME>\lib\ext下的扩展类,那么这些扩展类也会由TestClassLoader加载,但是会报类文件找不到的情况。但是自定义类加载器也是能加载
<JAVA_HOME>\lib\ext下的扩展类的,只要自定义类加载器能找准扩展类的类路径。以扩展目录
com.sun.crypto.provider下的类举例:1、Demo中随便引用一个扩展类:
2、修改TestClassLoader#findClass:

3、测试代码中需要调用一下
Demo类的构造器:
4、运行测试代码
自定义类加载器成功加载了扩展类。

由上得出结论,
<JAVA_HOME>\lib\ext下的扩展类是没有强制只有ExtClassLoader能加载,自定义类加载器也能加载。1.3 一个比较完整的自定义类加载器
一般情况下,自定义类加载器都是继承
URLClassLoader,具有如下类关系图:
2、Tomcat破坏双亲委派机制
Tomcat 为什么要破坏双亲委派机制呢?
- 应用隔离需求:Tomcat 中可以部署多个 Web 项目,假设有两个 Web 应用程序,它们都有一个类,叫做 User,并且它们的类全限定名都一样,比如都是
com.yyy.User。若使用 JVM 默认的AppClassLoader加载 Web 应用,AppClassLoader只能加载一个 User 类,在加载第二个同名 User 类时,AppClassLoader会返回第一个 User 类的 Class 实例。为了保证每个 Web 项目互相独立,所以不能都由AppClassLoader加载。
- 热部署支持:不重启 JVM 的前提下能够重新加载修改后的类,标准机制无法实现类的动态替换。
为此 Tomcat 实现了自己的类加载器层次体系,如下图所示:

- WebappClassLoader:Tomcat 为每个 Web 应用创建一个类加载器实例
WebappClassLoader。每个 Web 应用自己的 Java 类和依赖的 JAR 包,分别放在WEB-INF/classes和WEB-INF/lib目录下,都是WebAppClassLoader加载的。WebappClassLoader继承自URLClassLoader,重写了findClass和loadClass,WebappClassLoader的父类加载器是CommonClassLoader
- SharedClassLoader:两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类?双亲委派机制的各子加载器都能通过父加载器去加载类,于是考虑把需共享的类放到父加载器的加载路径。应用程序即是通过该方式共享 JRE 核心类。Tomcat 搞了个类加载器
SharedClassLoader,作为WebAppClassLoader的父加载器,以加载 Web 应用之间共享的类。若WebAppClassLoader未加载到某类,就委托父加载器SharedClassLoader去加载该类,SharedClassLoader会加载指定目录($CATALINA_HOME/shared/lib)下的共享类,之后返回给WebAppClassLoader,即可解决共享问题。
- CatalinaClassLoader:如何隔离 Tomcat 本身的类和 Web 应用的类?两个类加载器是平行的,它们可能拥有同一父加载器,但两个兄弟类加载器加载的类是隔离的。于是,Tomcat 搞了
CatalinaClassLoader,专门加载 Tomcat 自身的类。
- CommonClassLoader:当 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办?共享依旧靠父子关系。再增加个
CommonClassLoader,作为CatalinaClassLoader和SharedClassLoader的父加载器。
总结:
CommonClassLoader能加载的类都可被CatalinaClassLoader、SharedClassLoader使用。
CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
3、线程上下文类加载器
JVM 默认情况下,若一个类由类加载器 A 加载,则该类的依赖类也由相同的类加载器加载。例如
Class.forName 的源码如下,看到会使用调用者的类加载器去加载指定类。但这种模型在某些场景下存在问题,例如:
- SPI(Service Provider Interface)场景:基础接口由核心类库定义(如JDBC、JNDI 等),但实现由第三方提供。例如 JNDI 是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3 时放进去的
rt.jar),但 JNDI 的实现由独立厂商实现并部署在应用程序的 ClassPath 下,但启动类加载器不可能去加载 ClassPath 下的类。
- 反向依赖问题:高层类需要加载低层类时,传统的委派模型无法满足。
解决方案是线程上下文类加载器。线程上下文类加载器为每一个线程设置上下文类加载器(通过
java.lang.Thread#setContextClassLoader 方法),在该线程后续执行过程中再把这个类加载器取出来使用(通过java.lang.Thread#getContextClassLoader 方法)。如果创建线程时未设置上下文类加载器,将会从父线程(parent = currentThread())中获取,如果在应用程序的全局范围内都没有设置过,就默认是应用程序类加载器。使用示例
线程上下文类加载器的出现就是为了方便破坏双亲委派。典型应用场景有:
- SPI 机制:Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
- OSGi 模块化系统:OSGi(Open Service Gateway Initiative)是一种面向Java的动态模块化系统规范,它允许开发者将应用程序分解为可独立部署、管理的模块(称为 bundle)。每个 bundle 有独立的类加载器,实现类隔离。
四、常见问题
如何判断两个 Class 对象是否同一个类?
- 全限定类名相同。
- 加载这个类的类加载器相同。
即一个类的唯一性由加载它的类加载器和这个类的本身决定(类的全限定名+类加载器的实例ID作为唯一标识)。比较两个类是否相等(包括Class对象的
equals()、isAssignableFrom()、isInstance()以及instanceof关键字等),即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,这两个类就必定不相等。Class.forName 和 ClassLoader.loadClass 的区别
类加载过程:加载 —> 验证 —> 准备 —> 解析 —> 类初始化 —> 使用(对象实例初始化) —> 卸载。
java.lang.Class.forName 会调用到 forName0 方法,第二个参数 initialize = true,意为会进行类初始化(<clinit>())操作。java.lang.ClassLoader.loadClass 会调用到 protected 修饰的 loadClass(String name, boolean resolve),第2个参数resolve=false,意为不进行类的解析操作,也就不会进行类初始化,包括静态变量的初始化、静态代码块的运行,都不会进行。- Author:mcbilla
- URL:http://mcbilla.com/article/544bd7c5-5237-4fbf-97fb-97f8d23e91b4
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts
