深入理解JVM_9_类加载器

引:之前一直在说类加载,类加载就需要类加载器,类加载最初是为了满足Java Applet,现在基本已经死掉了,但是类加载却在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为Java体系中一块重要的基石,可谓失之桑榆,收之东隅。

什么是类加载器?

虚拟机设计团队把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己去决定如何去获取所需要的类,实现这个动作的代码模块称为“类加载器”。

同一个Class文件,不同的类

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。换个说法:比较两个类是否“相等”,只是在这两个类是由同一个类加载器加载的前提下才有意义,如果这两个类源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

这里的相等包括:代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法返回结果,下面的代码展示了不同的类加载器对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
public class ClassLoaderTest {

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("com.todorex.demo.ClassLoaderTest").newInstance();

System.out.println(obj.getClass());

System.out.println(obj instanceof com.todorex.demo.ClassLoaderTest );
}

}

运行结果:

1
2
class com.todorex.demo.ClassLoaderTest
false

这里的false就证明了两个类虽然来自于同一个Class文件,但是由于使用的类加载器不同,就依然是两个独立的类。

双亲委派模型

类加载器的类型

Java虚拟机角度

  1. 启动类加载器,它本身是由C++语言实现,或者底层的关键方法是用C实现的。
  2. 所有其他类加载器,这些类加载器都由Java语言实现。

Java开发人员角度

  1. 启动类加载器:这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数所指定的路径找那个的,并且是虚拟机识别的(按照文件名识别的)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接使用,用户在编写自定义类加载器时,如果需要把加载请求委派给引导类加载器,直接使用null即可。(不知道怎么用,知道的大佬请告知!!!!)
  2. 扩展类加载器:这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以使用扩展类加载器。
  3. 应用程序类加载器:这个类加载器由sun.misc.Launcher$AppClassLoader实现,也称系统类加载器。它负责加载用户所指定的类路径java -classpath或-Djava.class.path的所有类,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

类加载器的双亲委派模型

模型图

Parents Delegation Model

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。(组合就是在一个类中调用另一个类的代码)

工作过程

我相信这张图最清楚了:

双亲委派模型工作流程

我们可以由图看到以下过程:

  1. 自底向上检查类是否已经加载,若已加载,直接返回。
  2. 若所有父类都没有加载该类,则自顶向下尝试加载该类。
  3. 如果加载不成功,则抛出ClassNotFoundException异常。

我们可以从代码看看他是怎么实现的?

  1. 先看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
    36
    37
    38
    protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
    synchronized (getClassLoadingLock(name)) {
    // 先从缓存查找该class对象,找到就不用重新加载
    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
    // 如果都没有找到,则通过自定义实现的findClass去查找并加载
    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;
    }
    }

从该函数我们可以得知双亲委派模型会先递归去查找父加载器是否已经加载过该类了。如果父加载器都没有加载过该类,则开始调用fandClass
()尝试去加载该类。由于启动类加载器不可知,我们可以去看看扩展类加载器的findClass()。我们可以看到下面的类图:

ExtClassLoader

我接着我想去看看ExtClassLoader类里面的findClass()方法,结果发现没有,只要去他的父类URLClassLoader去找找,还好找到了,可以看看下面的代码:

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
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}

其实AppClassLoader的findClass()也是继承自URLCLassLoader,所以都是一样的,我们再结合最开始的loadClass()就可以很好的理解了上面双亲委派模型的工作流程了。

好处

java随着它的类加载一起具备了一种带有优先级的层次关系,它能保证一个类在程序中各种类加载器环境中都是同一个类。

双亲委派模型的“双亲”

在Java虚拟机英文文章里双亲委派模型的英文是Parent-Delegation Model,不知道为什么中文翻译会称他为双亲委派模型,可能是他一般都会找到一个爸爸去委托去处理吧。

破坏双亲委派模型

目前为止,双亲委派模型主要出现过3次较大的“被破坏”的情况。

  1. 在JDK1.2之前,新建加载器都是通过重写loadClass()方法来区分不同的加载器,以及修改加载逻辑,这样就破坏了双亲委派模型的向上寻找父加载器去加载的规范,在JDK1.2之后为了向前兼容,ClassLoader添加了新的protect方法findCLass()方法,从而实现了在双亲委派模型上实现加载逻辑的修改。
  2. 线程上下文类加载器,如JNDI服务(没用过)
  3. 实现动态性,如OSGi(没用过)

总结

有时候分析东西查看源码是必要的,还要利用一些工具去分析他们的继承关系。

参考

  1. 《深入理解Java虚拟机》
  2. 深入理解Java类加载器(ClassLoader)
  3. 深入理解Java类加载器(一):Java类加载原理解析