类加载器

类加载阶段中通过类的全限定名来获取描述此类的二进制字节流的动作放到Java虚拟机外部去实现,以便让程序自己决定如何去获取所需的类,实现该动作代码模块称为类加载器

类加载器是Java技术体系的重要基石,是Java语言的一项创新,也是Java语言流行的重要原因之一,最初是为了满足Java Applet需求而开发的,但目前Java Applet基本上已经死掉,但类加载器却在类层次划分OSGi热部署代码加密等领域大放异彩。

类与类加载器

虽然类加载器只用于实现类的加载动作,但在Java程序中的作用远远不限于类加载阶段

任意一个类都需要由加载它的类加载器和该类本身一同确立其在Java虚拟机中的唯一性,每个类加载器都有一个独立类名称空间

比较两个类是否相等,只有两个类是由同一个类加载器加载的前提下才有意义,即使两个类源于同一个Class文件,被同一个虚拟机加载,若加载它们的类加载器不同,这两个类就一定不相等,包括代表类的Class对象的equals()方法、isAssignableFrom()方法isInstance()方法的返回结果、instanceof关键字做对象所属关系判定等情况。

类加载器初始化过程

Java命令执行代码大体流程

sun.misc.Launcher初始化使用单例模式设计,在Launcher构造方法内创建了sun.misc.Launcher.ExtClassLoader扩展类加载器sun.misc.Launcher.AppClassLoader应用类加载器,Launcher的getClassLoader()方法默认返回的类加载器AppClassLoader的实例加载开发人员写的应用程序。

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
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;

public static Launcher getLauncher() {
return launcher;
}

public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 构造扩展类加载器,在构造过程中将其父加载器设置为null
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 构造应用类加载器,在构造过程中将其父加载器设置为ExtClassLoader
// Launcher的loader属性值是AppClassLoader,一般用这个类加载器来加载自己写的应用程序
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}

public ClassLoader getClassLoader() {
return this.loader;
}

双亲委派模型

Java虚拟机的角度讲,只存在两种不同的类加载器

  • 启动类加载器Bootstrap ClassLoader,该类加载器使用C++语言实现,是虚拟机自身的一部分
  • 所有的其他的类加载器,这些类加载器都由Java语言实现独立于虚拟机外部,其全都继承自抽象类java.lang.ClassLoader

类加载器还可以划分得跟细致一点,一共有三种系统提供的类加载器:

  • 启动类加载器Bootstrap ClassLoader,负责将<JAVA_HOME>\lib目录中或被-Xbootclasspath参数所指定的路径中的,且是虚拟机识别仅按照文件名识别的类库加载到虚拟机内存中。无法Java程序直接引用,自定类加载器时,若需把加载请求委派引导类加载器,直接用null代替。
  • 扩展类加载器Extension ClassLoadersun.misc.Launcher$ExtClassLoader实现,负责加载<Java_Home>/lib/ext或被java.ext.dir系统变量指定路径中的所有类库,开发者可直接使用扩展类加载器。
  • 应用程序类加载器Application ClassLoadersun.misc.Launcher$AppClassLoader实现。该类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,一般称为系统类加载器,负责加载用户类路径ClassPath上所指定的类库,开发者可直接使用这个类加载器,若应用程序中未自定义自己的类加载器,一般情况作为程序中默认的类加载器

应用程序都是由这3种类加载器互相配合进行加载的,若有必要可加入自定义类加载器。如下图所示,类加载器之间的这种层次关系,称为类加载器的双亲委派模型

类加载器双亲委派模型

双亲委派模型要求除顶层启动类加载器外,其余类加载器都应当有自己的父类加载器。这里的父子关系一般不会以继承关系来实现,而是使用组合关系来复用父类加载器的代码。并不是强制性的约束模型,是设计者推荐的一种类加载实现方式。

若类加载器收到类加载请求,首先不会自己尝试加载该类,而是把该请求委派给父类加载器去完成,每个层次的类加载器都是如此所有的类加载请求最终都应该传送到顶层的启动类加载器中,只有父类加载器反馈自己无法完成该加载请求时,即其搜索范围中没找到所需的类,子加载器才会尝试自己加载

双亲委派模型可以使Java类随其类加载器一起具备一种带有优先级关系的层次关系。如java.lang.Object无论哪个类加载器加载,最终都会委派给处于模型顶端的启动类加载器进行加载,因此Object在各种类加载器环境中都是同一个类

双亲委派模型实现代码集中在java.lang.ClassLoaderloadClass()方法中,实现简单,其对于保证Java程序稳定运作很重要。

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<?> 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();
// 调用URLClassLoader的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;
}
}

全盘负责委托机制当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类依赖及引用类也由该ClassLoder载入。

设计双亲委派机制的目的

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

破坏双亲委派模型

第一次破坏

双亲委派模型是在JDK1.2之后引入的,而类加载器抽象类java.lang.ClassLoader则在JDK1.0已经存在为了向前兼容JDK1.2之后的java.lang.ClassLoader添加了一个protected方法findClass()

在此之前,用户继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,虚拟机在进行类加载时会调用加载器的私有方法loadClassInternal(),该方法仅仅去调用自己的loadClass()方法。

JDK1.2之后不提倡覆盖loadClass()方法,提倡将类加载逻辑写到findClass()中,在loadClass()中若父类加载失败,再调用findClass()中自己的逻辑完成加载。这样即可保证新写的类加载器符合双亲委派规则

第二次破坏

双亲委派模型对于越基础的类由越上层的加载器进行加载,从而很好的解决了各个类加载器的基础类的统一问题,但有一个缺陷,双亲委派模型并不能解决基础类又需要回调用户代码的情况。

JNDI服务现已经是Java标准服务,其代码由启动类加载器加载,但JNDI的目的是对资源进行集中管理和查找,需要调用独立厂商实现并部署在应用程序中ClassPathJNDI接口提供者的代码

为了解决该问题,引入了一个不太优雅的设计:线程上下文类加载器Thread Context ClassLoader)。

该类加载器可通过java.lang.Thread类的setContextClassLoader()方法进行设置线程上下文类加载器,若创建线程时未设置,将会从父线程中继承一个,若应用程序全局范围内都未设置线程上下文类加载器,则线程上下文类加载器默认为应用程序类加载器。

JNDI服务使用线程上下文类加载器去加载所需的JNDI接口提供者的代码,其实就是通过父类加载器请求子类加载器去完成类加载。该方式打通了双亲委派模型的层次结构来逆向使用类加载器,违背了双亲委派模型一般原则

Java中所有涉及SPI的加载基本都采用此种方式,如JNDIJDBCJCEJAXBJBI

第三次破坏

由于用户对程序动态性代码热替换模块热部署)的追求,目前OSGi是业界事实上模块化标准OSGi实现模块化热部署的关键在于其自定义的类加载器机制的实现,每个模块(OSGi称为Bundle)都有一个自己的类加载器,更换模块时连同类加载器一起替换以实现代码热替换。

OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而进一步发展成了网状结构。OSGi类搜索顺序如下:

  • java.*开头的类委派给父类加载器加载
  • 否则,将委派列表名单中的类委派给父类加载器加载
  • 否则,将Import列表中的类委派给ExportBundle的类加载器加载
  • 否则,查找当前BundleClassPath,使用自己的类加载器加载
  • 否则,查找类是否在自己的Fragment Bundle中,在,则委派给Fragment Bundle的类加载器加载
  • 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
  • 否则,类查找失败

查找顺序中,只有第一二两条符合双亲委派规则,其余都是在平级的类加载器中进行。

自定义类加载器

自定义类加载器只需要继承java.lang.ClassLoader类,该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制,还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器主要是重写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
36
37
38
39
public class ElevenClassLoader extends ClassLoader {

private String classPath;

public ElevenClassLoader(String classPath) {
this.classPath = classPath;
}

private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] bytes = new byte[len];
fis.read(bytes);
fis.close();
return bytes;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}

public static void main(String[] args) throws Exception {
ElevenClassLoader classLoader = new ElevenClassLoader("E:\\IData");
Class clazz = classLoader.loadClass("com.eleven.icode.jvm.entity.User");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sout", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}

}

上面代码仅仅是自定义了自己的类加载器,仅当类AppClassLoader类加载器在项目中找不到User的字节码时,才会使用ElevenClassLoader加载User类。为了打破这种双亲委派机制,还必须重写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
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (!name.startsWith("com.eleven.icode.jvm")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(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) {
resolveClass(c);
}
return c;
}
}

由于所有的类都是继承自Object类,故if (!name.startsWith("com.eleven.icode.jvm"))判断尤为重要,若去掉该类,会报java.io.FileNotFoundException: E:\IData\java\lang\Object.class (系统找不到指定的文件。)异常。若将rt.jar中的Object.class拷贝到该路径,会触发上面说的沙箱安全机制:java.lang.SecurityException: Prohibited package name: java.lang

Tomcat打破双亲委派机制

Tomcat是个web容器,一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,要保证相互隔离。还要保证部署在同一个web容器中相同的类库相同的版本可以共享。否则若服务器有10个应用程序,则要有10份相同类库加载进虚拟机。

web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。web容器要支持jsp的修改,而jsp文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器,重新创建类加载器,重新加载jsp文件

Tomcat自定义类加载器

CommonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问
CatalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见
SharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见
WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的,WebappClassLoader实现相互隔离,比如不同war包应用引入了不同的spring版本,这样实现就能加载各自的spring版本;每个WebappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制
JspClassLoader:加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,出现目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JspClassLoader实例,再建立一个新的Jsp类加载器来实现JSP文件的热加载功能