[读书笔记]Java类加载器

一、类与类加载器

类加载器除了在类加载阶段的作用外,还确定了对于一个类,都需要由加载它的类加载器和这个类本身一同确定其在Java虚拟机中的唯一性。通俗一点来讲,要判断两个类是否“相等”,前提是这两个类必须被同一个类加载器加载,否则这个两个类不“相等”。
这里指的“相等”,包括类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法、instanceof关键字等判断出来的结果。

示例:不同的类加载器对instanceof关键字结果的影响

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
package org.kesar;

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest
{

/**
* @param args
* @throws Exception
*/

public static void main(String[] args) throws Exception
{

ClassLoader myLoader = new ClassLoader()
{
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException
{
try
{
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null)
{
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
}
catch (IOException e)
{
throw new ClassNotFoundException(name);
}
}
};
Object obj=myLoader.loadClass("org.kesar.ClassLoaderTest").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof ClassLoaderTest);
}
}

输出结果:
class org.kesar.ClassLoaderTest
false

结果分析:
由于ClassLoaderTest在虚拟机中存在两个,一个是由系统应用程序类加载器加载,另一个是由我们自定义的类加载器加载的,所以两个类并不“相等”。

二、双亲委派模型

从Java虚拟机来讲的话,目前的类加载器有不同的两种。一种是启动类加载器(Bootstrap ClassLoader),是虚拟机中的一部分;另一种是其他的所有类加载器,独立于虚拟机外部的,继承自抽象类java.lang.ClassLoader
从系统提供的类加载器来讲,有这3种类加载器:启动类加载器、扩展类加载器、和应用程序类加载器。

1. 系统的类加载器

(1)启动类加载器(Bootstrap ClassLoader)
加载内容:<JAVA_HOME>\lib目录、被-Xbootclasspath参数所指定的路径中的虚拟机可识别的类库加载到虚拟机内存中。
特点:不能被Java程序直接引用,如果想将加载委托请求给启动类加载器加载,直接使用null代替即可。

示例:java.lang.Class的getClassLoader()源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Returns the class loader for the class. Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
*/

public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}

(2)扩展类加载器(Extension ClassLoader)
加载内容:<JAVA_HOME>\lib\ext目录、被java.ext.dirs系统变量所指定的路径中的所有类库
特点:使用Java编写,开发者可以直接使用扩展类加载器

(3)应用程序类加载器(Application ClassLoader)
加载内容:加载用户类路径(ClassPath)上所指定的类库
特点:开发者可以直接使用这个类加载器,如果没有自定义的类加载器,将默认使用它。

2. 双亲委派模型(Parents Delegation Model)

工作过程:当类加载器受到类加载请求时,一般不会自己去加载这个类,将会将这个类加载请求委派父类加载器去完成,每一层次的类加载器都是这么做的,只有当这个类没有父类加载器时(Bootstrap ClassLoader)或父类加载器反馈无法加载这个类时(搜索范围中没有找到所需的类),子加载器才会自己去加载。

类的双亲委派模型如下图:

好处:使用双亲委派模型,由于其工作过程的特点,Java类随着它的类加载器一起具备了一种带有优先级的层次关系(层次越高越优先加载类)。比如:java.lang.Object,每个类都默认继承自这个类,故每次类加载都会加载这个类,而该类存在rt.jar中,而使用双亲委派模型,每次类价值请求都将会委派到启动类加载器,故将可以成功加载到java.lang.Object。而且java.lang.Object每次的委派都是被启动类加载器加载,这样保证了类在虚拟机中的一致性。如果失去这种机制,每个类加载器将自己加载一个java.lang.Object,那么系统中将会有多个不相等的Object类,将会导致应用程序一片混乱。

可以看看ClassLoader的loadClass()方法源码:

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
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
// 说明父类加载器无法完成加载请求
}

if (c == null) {
// 如果父类加载器找不到,则自己进行类加载
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) {
resolveClass(c);
}
return c;
}
}

3. 三次破坏双亲委派模型

(1)第一破坏:JDK版本问题
问题:JDK1.2之前是没有双亲委派模型的。JDK1.0时ClassLoader就已经存在,这时就可以自定义类加载器了,是要重写ClassLoader的findClass()方法来实现的。但是在引入双亲委派模型后,重写findClass()一不小心就会破坏双亲委派模型。
处理:JDK1.2后,为ClassLoader增加一个可重写的loadClass()方法,规定以后自定义类加载器就重写loadClass()就可以,避免重写findClass()破坏双亲委派模型。

(2)第二破坏:模型自身缺陷问题
问题:双亲委派模型无法从父类加载器去请求子类加载器委派加载类。比如有一个典型的例子JNDI服务,JNDI现在已经是Java的标准服务,它的代码是由启动类加载器去加载的,需要调用应用程序在ClassPath下的JNDI接口提供者的代码,但启动类加载器不“认识”这些代码。
处理:使用了线程上下文类加载器(Thread Context ClassLoader),使得父类加载器可以请求子类加载器去完成类加载的动作,达到了逆向使用类加载器的效果,打通双亲委托模型的层次结构。

(3)第三破坏:“动态性”问题
问题:程序动态性的发展:代码热替换、模块热部署等。
处理:定制了Java模块化标准OSGI,在这种环境下,将不会是双亲委派模型的树状结构了,而是进一步发展为网结构。

附:OSGI顺序执行过程
1) 将java.*开头的类委托给父类加载器加载。
2) 否则,将委托列表名单内的类委派给父类加载器加载。
3) 否则,将Import列表中的类委托给Export这个类的Bundle的类加载器加载。
4) 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
5) 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
6) 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
7) 否则,类查找失败。