Java类加载器详解

Java三种类型的类加载器

  • 我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM 启动的时候,Java 缺省开始使用如下三种类型类装入器:
  • 启动类加载器(Bootstrap Class Loader):引导类装入器是用本地代码实现的类装入器,它负责将 /lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
  • 标准扩展类加载器(Extensions Class Loader):扩展类加载器是由 Sun 的 ExtClassLoader (sun.misc.Launcher$ExtClassLoader) 实现的。它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
  • 系统类加载器(System Class Loader ):系统类加载器是由 Sun 的 AppClassLoader (sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径(classpath)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

类加载双亲委派机制介绍

为什么是双亲委派机制:java.lang.String类在rt.jar中,在JVM启动时由BootStrap启动类加载器加载;当用户自定义的加载器也需要加载java.lang.String类时,如果自定义的加载器不请求双亲加载(检查双亲是否已经加载),就会有2个java.lang.String类了。所以双亲委派机制解决了这个问题:一个类加载器在接收到类加载请求的时候,首先将其委托给父类加载器,如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成加载任务时,才去自己加载。

双亲委派机制很好解决了各个类加载器类加载的统一问题。
Alt text
系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
try {
System.out.println(ClassLoader.getSystemClassLoader());
System.out.println(ClassLoader.getSystemClassLoader().getParent();
System.out.println(ClassLoader.
getSystemClassLoader().getParent().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}

通过ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器,系统类加载器父类是扩展类加载器,扩展类加载器的父类则是启动类加载器,在JVM中的体现是null,因为启动类加载器是用native的c++实现的。以上代码结果如下:

1
2
3
sun.misc.Launcher$AppClassLoader@197d257
sun.misc.Launcher$ExtClassLoader@7259da
null

通过以上的代码输出,我们可以判定系统类加载器的父加载器是扩展类加载器,但是我们试图获取启动类加载器的父类加载器时确得到了null,就是说扩展类加载器本身强制设定父类加载器为null。我们还是借助于代码分析一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected ClassLoader() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
this.parent = getSystemClassLoader();
initialized = true;
}

protected ClassLoader(ClassLoader parent) {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkCreateClassLoader();
}
//强制设置父类加载器
this.parent = parent;
initialized = true;
}

线程上下文类加载器(Thread Context Claass Loader,TCCL)

双亲委派机制解决了类重复加的问题,但是不能解决应用开发中遇到的全部类加载问题。双亲委派机制中,上层父类的加载器不可以使用子类加载的对象。而有些时候程序的确需要父类调用子类对象,这时候就需要线程上下文加载器来处理。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。 以JNDI为例,它的核心接口是由JRE核心类(rt.jar)实现的,由启动类加载器加载。但是在这核心类JNDI的实现是由第三方厂商实现的,由系统类加载器加载。启动类加载器是无法找到第三方实现类,因为它只加载 Java 的核心库。

但这些核心接口的实现类必须能加载由第三方厂商提供的JNDI实现。这种情况下父类请求子类加载器去完成类加载任务(这个类只有子类加载器可见),双亲委派机制就会失效。解决办法就是让核心JNDI类使用线程上下文类加载器,从而有效的打通类加载器层次结构,逆着代理机制的方向使用类加载器。

线程上下文类加载器(Thread Context ClassLoader)
线程上下文加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,默认是系统类加载器。

Thread Context Class Loader(TCCL)

TCCL is a hack that was added in Java 1.2 to support J2EE. Specifically it was needed to support things like Entity Beans; in a modern world it’s used to support technologies like JPA, JAXB, Hibernate and so on.

先来看个TCCL的例子,自定义类加载器myUrlCl(URLClassLoader)

1
2
3
4
5
6
7
8
9
10
11
12
public class MyURLClassLoaderTest{
public static void main(String[] args) throws Exception {
URL[] baseUrls = new URL[]{new URL("file:D:/java_tools/maven2/m2/repository/com/xxx/common/common-test/1.0-SNAPSHOT/common-test-1.0-SNAPSHOT.jar")};
String targetClassName = "com.xxx.common.DocumentBuilderFactoryImpl";
ClassLoader systemCl = ClassLoader.getSystemClassLoader();
ClassLoader myUrlCl = new URLClassLoader(baseUrls);
Thread.currentThread().setContextClassLoader(myUrlCl);
DocumentBuilderFactory targetFactoryObj = DocumentBuilderFactory.newInstance(targetClassName,null);
System.out.println(targetFactoryObj.getAttribute(null));
System.out.println("调用类的加载器: "+systemCl.toString());
}
}

其中com/xxx/common/common-test/1.0-SNAPSHOT 这个jar包有 DocumentBuilderFactoryImpl.java 类和Apple.java 类

1
2
3
4
5
6
D:\CODE\COMMON-TEST\SRC\MAIN\JAVA
└─com
└─xxx
└─common
Apple.java
DocumentBuilderFactoryImpl.java

DocumentBuilderFactoryImpl类继承SPI接口javax.xml.parsers. DocumentBuilderFactory. DocumentBuilderFactory

1
2
3
4
5
6
7
8
9
10
11
12
public class DocumentBuilderFactoryImpl extends DocumentBuilderFactory {
@Override
public Object getAttribute(String name) throws IllegalArgumentException {
StringBuilder sb = new StringBuilder();
sb.append("系统类加载器: "+ClassLoader.getSystemClassLoader()+"\n");
sb.append("线程上下文加载:"+Thread.currentThread().getContextClassLoader()+"\n");
sb.append("Apple类加载器:"+Apple.class.getClassLoader()+"\n");
sb.append("DocumentBuilderFactoryImpl类的加载器: "+this.getClass().getClassLoader());
return sb.toString();
}
// 其余override方法省略
}

执行MyURLClassLoaderTest结果是:DocumentBuilderFactoryImpl内部的系统类加载器是AppClassLoader@4e7a15b和调用类的系统类加载器一样;DocumentBuilderFactoryImpl实现类加载器URLClassLoader@264430e2,TCCL和Apple类的加载器也是URLClassLoader@264430e2。

执行的结果表明:调用类(MyURLClassLoaderTest.java)的SystemClassLoader无法找到第三方实现类DocumentBuilderFactoryImpl,只通过设置TCCL,请求myUrlCl来加载DocumentBuilderFactoryImpl;另一方面我们看到Apple类的加载器也是myUrlCl,为什么呢?因为JVM关于类加载的一个规范是:new和Class.forName(String)默认使用的是调用类的加载器!!

1
2
3
4
5
系统类加载器:                            sun.misc.Launcher$AppClassLoader@4e7a15b
线程上下文加载: java.net.URLClassLoader@264430e2
Apple类加载器: java.net.URLClassLoader@264430e2
DocumentBuilderFactoryImpl类的加载器: java.net.URLClassLoader@264430e2
调用类的加载器: sun.misc.Launcher$AppClassLoader@4e7a15b

有一个思考点留给读者,如果MyURLClassLoaderTest中注释掉以下代码,执行结果是怎么样的?

1
Thread.currentThread().setContextClassLoader(myUrlCl);

源码解读

1
2
3
4
5
6
7
8
9
public static DocumentBuilderFactory newInstance(String factoryClassName, ClassLoader classLoader){
try {
//classLoader为null
return (DocumentBuilderFactory) FactoryFinder.newInstance(factoryClassName, classLoader, false);
} catch (FactoryFinder.ConfigurationError e) {
throw new FactoryConfigurationError(e.getException(),
e.getMessage());
}
}

继续深入查看FactoryFinder.newInstance(factoryClassName, classLoader, false)方法

1
2
3
4
static Object newInstance(String className, ClassLoader cl, boolean doFallback)
throws ConfigurationError{

return newInstance(className, cl, doFallback, false);
}

newInstance方法先获取Provider(DocumentBuilderFactoryImpl)类加载器,然后调用newInstance创建实例对象

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
static Object newInstance(String className, ClassLoader cl, boolean doFallback, boolean useBSClsLoader) throws ConfigurationError{
// make sure we have access to restricted packages
if (System.getSecurityManager() != null) {
if (className != null && className.startsWith(DEFAULT_PACKAGE)) {
cl = null;
useBSClsLoader = true;
}
}
try {
// 获取SPI的ProviderClass
Class providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
Object instance = providerClass.newInstance();
if (debug) { // Extra check to avoid computing cl strings
dPrint("created new instance of " + providerClass +
" using ClassLoader: " + cl);
}
return instance;
}
catch (ClassNotFoundException x) {
throw new ConfigurationError(
"Provider " + className + " not found", x);
}
catch (Exception x) {
throw new ConfigurationError(
"Provider " + className + " could not be instantiated: " + x,
x);
}
}

继续看getProviderClass方法

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
static private Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{

//参数情况:cl=null,doFallback=false,useBSClsLoader=false
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
//最终classloader的获取落到了ss.getContextClassLoader方法
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}else {
return cl.loadClass(className);
}
}
}else {
//如果指定了classloader,就使用指定的加载该类
return cl.loadClass(className);
}
}catch (ClassNotFoundException e1) {
if (doFallback) {
// 异常则使用当前的ClassLoader
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}else {
throw e1;
}
}
}

最后的最后看getContextClassLoader方法,classloader为TCCL

1
2
3
4
5
6
7
8
9
10
11
12
ClassLoader getContextClassLoader() throws SecurityException{
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
ClassLoader cl = null;
// 最终classLoader获取来自TCCL
cl = Thread.currentThread().getContextClassLoader();
if (cl == null) //极端情况,一般不会到这步
cl = ClassLoader.getSystemClassLoader();
return cl;
}
});
}

Tomcat的类加载

栗子和源码已经演示完毕,是不是有点迷惑:为什么不直接工程中依赖common-test-1.0-SNAPSHOT的jar包,这种类型的ClassLoader设计应用场景是什么?除了SPI接口应用了TCCL之外,另一个应用场景是Tomcat的启动类也应用了这种特性。
tomcat_classloader
每一个WebApp Class Loader都会有个统一的启动方法start(或者init),Catalina ClassLoader(类似于上例中的MyURLCl)加载WebAppClassLoader,这样就能做到每个WebApp间接依赖的下游的类,即使类限定名一直也不会发生冲突,这就是类隔离的。

JVM唯一标识一个类:类加载器+类限定名

关于Tomcat类加载器机制会有专门的一篇博文,敬亲期待!