类与类加载器
对于一个类而言,必须由加载它的类加载器和这个类本身 一起共同确立其在JVM中的唯一性。
-
类本身:这里指的是一个类的二进制定义 ,通常就是那个
.class文件。它由全限定名 (如com.example.MyClass)来标识。 -
类加载器:这是
JVM中负责将这个.class文件的二进制字节流加载到内存,并将其转换为Class对象的一个功能模块 。每一个Class对象在堆中创建时,都会记录加载它的那个类加载器。
为什么
JVM中需要两者来共同确定唯一性?因为同一个
class文件可以被不同的类加载器加载,因此在JVM看来由不同类加载器加载的同一个class文件,是完全不同的两个类。
每一个类加载器都拥有一个独立的类名称空间,这样类加载和类一起实现唯一性。
双亲委派机制
从虚拟机的视角来看,只有两种不同的类加载器:
- 启动类加载器(
BootstrapClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分。 - 其他类加载器,这些类加载器都由
java语言实现,独立存在于虚拟机外部,并且全部继承自抽象类ClassLoader。
从开发人员角度来看,类加载器会划分的更细致一些,一直保持着三层类加载器 、双亲委派的类加载架构。
三层类加载器:
- 启动类加载器:这个类加载器负载加载存放在
JAVA_HOME\lib目录下的类,启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果想把加载请求委派给引导类加载器处理,直接使用null即可。
java
// Returns the class's class loader, or null if none.
static ClassLoader getClassLoader(Class<?> caller) {
// 返回null代表使用BootstrapClassLoader
if (caller == null) {
return null;
}
// Circumvent security check since this is package-private
return caller.getClassLoader0();
}
- 扩展类加载器:这个类加载器是以
Java代码的形式实现的,它负责加载JAVA_HOME\lib\ext目录中的所有类库。这是一种Java系统类库的扩展机制,JDK开发团队允许用户将具有通用性的类库放置在ext目录里以扩展Java SE的功能。 - 应用程序类加载器:也被称为系统类加载器,负责加载用户类路径上所有类库。

该图所描述的各个类加载器之间的层次关系就是所谓的双亲委派模型。
双亲委派模型 要求除了最底层的启动类加载器,其他加载器都应有自己的父类加载器。这种父子 关系并不是以继承的关系 来体现,而是使用组合的关系来复用父加载器的代码。
双亲委派模型 的工作过程是:如果一个类加载器收到了类加载的请求,它首先并不会自己去加载这个类,而是把这个请求委托给父类加载器去完成,每一层加载器都是如此,直到最顶层的启动类加载器,只有当父加载器反馈自己无法完成这个加载请求的时候(它的搜索范围没有找到这个类),子类加载器才会尝试自己去加载。
那么使用双亲委派模型的好处是什么呢?
Java中的类随着它的类加载器一起具备了一种带有优先级 的层次关系。比如类java.lang.Object,它存放在rt.jar中,无论是哪一个类加载器要加载这个类,最终都会委托到顶层的启动类加载器进行加载,因此Object类在程序的各种类加载器都能够保证是同一个类,这确保了Java核心类库加载的正确性,并且保证类唯一。
而如果说没有使用双亲委派模型,都由各个类加载器自己去加载,那么如果用户自己也编写一个java.lang.Object类,并放在程序的ClassPath中,那么系统中将会有多个不同的Object类。
双亲委派模型的实现
双亲委派机制的实现其实异常简单。在ClassLoader类中的loadClass方法中实现了这一点。
java
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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
// 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();
c = findClass(name);
// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这个方法是用于根据类的全限定名 加载类并返回Class对象。
- 先加锁,防止同一个类被多个线程同时加载
- 使用
findLoadedClass来判断是否已经加载了这个类,JVM会缓存已经加载过的类 - 使用父类加载器优先加载(双亲委派机制的核心)
- 如果
parent存在,则使用父类加载器加载 - 如果
parent为null,则使用Bootstrap ClassLoader加载
- 如果
- 当父类加载器没找到对应的类时,自己调用
findClass加载,一般类加载会重写findClass方法。
破坏双亲委派机制
为什么我们需要破坏掉
JVM的这种标准呢?比如
Tomcat,一个Tomcat通常可以部署多个Web
bashwebapps/ ├─ app1/ │ └─ WEB-INF/lib/spring-4.jar └─ app2/ └─ WEB-INF/lib/spring-5.jar如果
Tomcat也使用双亲委派机制,那么两个应用的Spring类只有一个版本能够生效,并且应用之间会相互污染。
其打破的方法如下:
java
protected Class<?> loadClass(String name, boolean resolve) {
// 先在自己的 Web 应用里找
Class<?> c = findClass(name);
if (c != null) return c;
// 自己找不到,再交给父加载器
return super.loadClass(name, resolve);
}