0%

Tomcat源码分析————Tomcat类加载

类加载器

是什么

类加载的过程中,“通过一个类的全限定名来获取描述此类的二级制字节流”这个动作放到JVM虚拟机外部去实现,由用户程序自己去选择如何加载所需的类;实现此方法的类便是类加载器;

JVM中是如何判断两个Class是否相等的?

类加载器将类元数据装载至JVM。

对任意的一个类来说,都需要这个类本身和其类加载其共同确认在JVM中的唯一性;

比如,在一个程序中,存在类加载器ALoader与BLoader,同时存在唯一一个类,top.bubuzi.App;情况如下

1
2
3
Class<?> ALoadClass ALoader.loadClass('top.bubuzi.App');
Class<?> BLoadClass BLoader.loadClass('top.bubuzi.App');
System.out.println(ALoadClass == BLoadClass); // false

可以看见,及时是来着同源的类,在不同加载器下,类元信息并不相等;这就实现了加载器隔离;

双亲委派机制

在JAVA中提供了三种类型的系统类加载器

  • 启动类加载器(Bootstrap ClassLoader)

    由C++底层实现,属于JVM核心的一部分,用于加载**${JAVA_HOME}/lib**目录下的文件,或者被-Xbootclasspath参数所指定路径中的文件,该加载器只能加载特定名称的文件,如(rt.jar),并非目录下全部文件;该加载器无法被用户直接引用;

  • 扩展类加载器(Extension ClassLoader)

    sun.misc.Launcher.ExtClassLoader实现,它负责加载**${JAVA_HOME}/lib/ext**目录中的文件,或被java.ext.dirs系统变量所指定的路径中的类,开发者可以直接使用扩展类加载器;

    JDK1.8后ExtClassLoader被废弃,使用PlatformClassLoader

  • 应用程序类加载器(Application ClassLoader)

    也称系统类加载器,由sun.misc.Launcher.AppClassLoader实现。负责加载用户类路径(Class Path)上所指定的类库,开发者可以直接使用这个类加载器。一般程序默认使用该加载器

应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。加载流程如下图所示:

img

在JDK1.8版本以上,若代码如下:

1
2
3
4
5
6
7
public static void main(String[] args) {
ClassLoader classLoader = App.class.getClassLoader();
while (classLoader!=null){
System.out.println(classLoader);
classLoader = classLoader.getParent();
}
}

会出现打印:

1
2
jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
jdk.internal.loader.ClassLoaders$PlatformClassLoader@6e8dacdf

其中并没打印出PlatformClassLoader的父加载器启动类加载器(Bootstrap ClassLoader),因为启动类加载器属于JVM的一部分,是无法被用户直接引用到的;

Java类加载层级称为:双亲委派模型。它的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

为什么?

都知道java.lang.Object是java中所有类的父类,它存放在rt.jar之中,按照双亲委派模型,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。试想,如果没有使用双亲委派模型,由各个类加载器自行去加载,显然,这就存在很大风险,用户完全可以恶意编写一个java.lang.Object类,然后放到ClassPath下,那系统就会出现多个Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

源码分析

以下代码取自 JDK11java.lang.ClassLoader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 由JVM实现的启动类加载器
private native Class<?> findBootstrapClass(String name);

Class<?> findBootstrapClassOrNull(String name) {
if (!checkName(name)) return null;
return findBootstrapClass(name);
}

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 判断该类是否已被加载,返回该类的类元信息
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) {
}
// 再经历父加载和系统加载后,依旧为被正确加载类元信息
if (c == null) {
long t1 = System.nanoTime();
// 调用自身的加载函数
c = findClass(name);

PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
// 返回加载类的类元信息
return c;
}
}

总得来说,类加载过程分为三个阶段:

  • 查找对应类是否加载,若加载直接返回类元;
  • 若未加载,递归调用父类加载器的加载函数,直到父类为空时,再调用启动类加载器去加载;
  • 若上面均未加载到类元,则使用加载器本身的加载函数去加载类元信息;

打破双亲委派模型

核心实现:线程上下文类加载器(Thread Context ClassLoader)

这个类加载器可以通过java.lang.Thread类中的setContextClassLoader方法进行设置,若创建线程时还未设置,它将会从父线程中继承一个类加载器,如果全局均为配置,那么默认就是应用程序加载器(Application ClassLoader)

Donate comment here.