类加载器

Java 虚拟机执行某个类,首先需要把类文件加载到虚拟机内存中,这个动作是通过类加载器来完成的。class 文件被类加载器加载到虚拟机内存中到卸载出内存经过了加载-验证-准备-解析-初始化-使用-卸载七个阶段。

从 JVM 角度来看有两种类加载器:启动类加载器用户自定义类加载器。从开发人员角度来讲的话可分为四种:启动类加载器、扩展类加载器、应用程序类加载器、用户自定义类加载器。

启动类加载(Bootstrap ClassLoader)

使用 C 语言实现,是虚拟机的一部分,主要负责将存放在 ${JAVA_HOME}\lib 目录中的或者通过 -Xbootclasspath 参数指定路径中的并且可以被虚拟机识别(按照文件名识别) 的类库加载到虚拟机内存中。Java 程序不可以直接引用启动类加载器,开发者在自定义了自己的类加载器后,也要把类加载的请求委派给启动类加载器(双亲委派模型)。

扩展类加载器(Extension ClassLoader)

sun.misc.Launcher.ExtClassLoader 实现,主要负责加载${JAVA_HOME}\lib\ext 目录中或者被 java.ext.dir 系统变量指定路径的类库,开发人员可以直接使用扩展类加载器。

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

sun.misc.Launcher.AppClassLoader 实现,负责加载用户类路径(Class Path)上指定的类库,开发者可以直接使用该加载器,一般情况下用户没有指定自己的类加载器,该加载器是程序的默认加载器。java.lang.ClassLoader#getSystemClassLoader 方法的返回值就是该类加载器,所以也叫做系统类加载器。

用户自定义类加载器

开发人员可以通过继承 ClassLoader 来实现自己的加载器。

类加载模型-双亲委派模型

双亲委派模型是类加载器之间的一种层次关系,双亲委派模型要求除了启动类加载器外,其他的类加载器都需要有自己的父类加载器。这种父子关系不是通过继承来实现,而是通过组合模式来复用父类加载器的代码。

类加载器模型
类加载器模型

当前类加载器收到了加载类的请求,先判断是否有父类加载器可以加载这个类,将加载请求委派给父类加载器(直到启动类加载),当父类无法加载时,子类加载器再尝试自己去加载,子类加载器无法加载时则抛出ClassNotFoundException 异常。双亲委派模型的优点:

  • 防止一个类被重复加载(同一个 class 文件被不同的类加载器加载后是不同的类)
  • 保护 Java 的核心类库,避免用户自定义类似于java.lang.Objectjava.lang.Integer类,对应用程序有一定安全保证
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
* @param name 类全路径名:例如:java.lang.Integer
* @param resolve
* @return The resulting Class object
* @throws ClassNotFoundException If the class could not be found
*/
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) {
// 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
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//使用该方法链接类 c,装入引用类(超类、接口、字段、方法中使用的本地变量等)。
resolveClass(c);
}
return c;
}
}

protected Object getClassLoadingLock(String className) {
Object lock = this;
//用 ConcurrentHashMap 实现,当前类加载器是否注册实现了 ParallelLoaders 并行加载能力
if (parallelLockMap != null) {
//parallerLockMap 由内部类 ParallelLoaders 来初始化
//实现了并行加载,则把当前需要加载的类全名做key,new一个对象做value放入parallelLockMap,表示这个类在被加载
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
//直接讲当前类加载器对象返回,表示当前类加载器正在加载 className 这个类
return lock;
}

破坏双亲委派模型

双亲委派模型是一种推荐的类加载器实现方式,从上面的代码中可以看到,自定义类加载器的实现的加载方法主要是在 findClass() 方法中实现的,在 loadClass 方法中如果父类加载器加载失败,就可以调用 findClass() 来加载,这样就可以保证实现双亲委派模型。

双亲委派模型是为了解决各个类加载器对于核心类统一加载的问题,如果基础类库中部分类需要调用用户代码,则双亲模型就要被破坏了,例如JNDI、JDBC、JCE、JAXB、JBI等。利用线程上下文类加载器(Thead Context ClassLoader)实现。

代码热替换、模块热部署等技术。