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