0%

类加载器

[TOC]

类加载器

类与类加载器

类的唯一性是由类本身和加载他的类确定的,不同类加载器加载的类将被置于不同的命名空间

系统提供的类加载器是从本地磁盘加载 .class文件 的。如果,需要从远程网络或数据库中下载.class字节码文件,那就需要我们来挂载自定义的类加载器。

类加载器 类介绍

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等。

方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c) 链接指定的 Java 类。

loadClass(String name) 内部是通过调用 findLoadedClass(String name)findClass(String name)实现类加载的。

类加载器种类

BootStrapClassLoader(启动类加载器)

该类 是由C++编写的,涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。

加载$JAVA_HOME/jre/lib下面的类库(或者通过参数-Xbootclasspath指定)

ExtClassLoader(扩展类加载器)

该类的源代码在 sun.misc.Launcher.java中,继承自 URLClassLoader

加载$JAVA_HOME/jre/lib/ext下的类库(或者通过参数-Djava.ext.dirs指定)

AppClassLoader(应用程序类加载器)

也称 系统类加载器。该类的源代码同样在 sun.misc.Launcher.java中,继承自 URLClassLoader

加载 -classpath -cp -Djava.class.path 所指定的路径(即CLASSPATH)下的类库。
可以通过 ClassLoader.getSystemClassLoader()来获取它。

CustomClassLoader(自定义类加载器)

通过继承 java.lang.ClassLoader类可以自己自定义 一个类加载器。继承 java.lang.ClassLoader类时。
若未指定parent,jvm会自动将 AppClassLoader作为CustomClassLoader的parent。

ContextClassLoader(线程上下文类加载器)

线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。该类加载器可以打破双亲委派机制,用于请求子加载器去完成部分类的加载。

java.lang.Thread 中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。

比如 jdbc 接口,由于 jdbc 接口 是由jdk自带的,jdbc 接口的代码就会由 BootStrapClassLoader 加载的。
然而,通常情况下jdbc的实现类都是放在classpath路径下的,BootStrapClassLoader 是无法获取 jdbc 的实现类。
这时 ContextClassLoader 就派上用场了,通过 Thread.currentThread().getContextClassLoader() 获得类加载器,而该加载器属于 AppClassLoader(能够顺利的获取 classpath下的jdbc 的实现类)。

双亲委派模型

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

双亲委派优点

避免重复加载。

比如 object 类,其存放在 $JAVA_HOME/jre/lib 中,无论哪个类加载器加载 object,都会委派给 BootStrapClassLoader 加载,使得环境中的 object 始终是同一个。
但如果不是双亲委派机制,每个子类都自己加载 object,就会造成环境中存在多个 object 类

类的加载过程

双亲委派

以下分析 简要分析下 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
39
40
41
42
43
44
public abstract class ClassLoader {

protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 检查类是否已被装载过
Class<?> c = findLoadedClass(name);
//若未装载过,进入If语句
if (c == null) {
long t0 = System.nanoTime();
try {
// 先委托给 父类加载器,进行加载。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 若 父类加载器为 null,则委托给 BootstrapClassloader
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();
// 根据具体实现,查找类。AppClassLoader 继承自 URLClassLoader,因此查看 URLClassLoader 即可。
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;
}
}
}

URLClassLoader 的 findClass()

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
public class URLClassLoader extends SecureClassLoader implements Closeable {
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");
// 在 ucp(URLClassPath) 中 查找 对应的类
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 解析class字节流,将其转为 内存中的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;
}

}

URLClassPath 的 getResource() 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class URLClassPath {
public Resource getResource(String var1, boolean var2) {
if (DEBUG) {
System.err.println("URLClassPath.getResource(\"" + var1 + "\")");
}

int[] var4 = this.getLookupCache(var1);

URLClassPath.Loader var3;
// 遍历所有的 URLClassPath.Loader ,若找到需要加载的类的字节码文件,则返回 Resource。
for(int var5 = 0; (var3 = this.getNextLoader(var4, var5)) != null; ++var5) {
Resource var6 = var3.getResource(var1, var2);
if (var6 != null) {
return var6;
}
}
return null;
}
}

题外话: 通过分析源码后发现,URLClassPath 查找资源时,是通过遍历URLClassPath.Loader来查找类的。因此,URLClassPath.Loader 的遍历顺序决定了 class文件的加载时机。

可见性/隔离性

自定义类加载器拥有三个其本类加载器加载的所有类的可见性,但是处于不同分支的自定义类加载器相互之间不具有可见性。

所谓不可见即不能直接互相访问, 也就是即使它们装载同一个类,也会拥有不同的命名空间, 会有不同的Class实例。

Class.forName

Class.forName是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。

Class.forName(String name, boolean initialize, ClassLoader loader)

参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。

Class.forName(String className)

相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器

tomcat

  • CommonClassLoader: 加载 /common/*
  • CatalinaClassLoader: /server/*
  • SharedClassLoader: /shared/*
  • WebAppClassLoader: /WebApp/WEB-INF/* 中的 Java 类库。

其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。
WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。
JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

其他参考

https://my.oschina.net/huzorro/blog/96791

http://www.voidcn.com/article/p-zdksbzlw-tn.html

https://www.jianshu.com/p/946df4276662