读书笔记——《深入理解 Java 虚拟机》系列之类加载器与双亲委派模型

本贴最后更新于 2506 天前,其中的信息可能已经时移俗易

1.Java 类加载器

在 Java 中,类加载器是用来通过一个类的全限定名来获取描述此类的二进制字节流的代码模块。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性。换句话说,比较两个类是否相等,只有在这两个类是被同一个类加载器加载时才有意义,否则即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类必定不相等。

下面我们用一个例子来了解一下被不同类加载器加载的同一个类会有什么影响:

package com.wxueyuan.classloader;

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

public class ClassLoaderTest {
	public static void main(String[] args) throws Exception{
		ClassLoaderTest instance = new ClassLoaderTest();
		ClassLoader myLoader = new ClassLoader() {
			@Override
			public Class<?> loadClass(String name) throws ClassNotFoundException {
				// TODO Auto-generated method stub
				String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
				InputStream is = getClass().getResourceAsStream(fileName);
				if(is == null) return super.loadClass(name);
				byte[] b = null;
				try {
					b = new byte[is.available()];
					is.read(b);
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				return defineClass(name, b, 0,b.length);
			}
		};
		Object obj = myLoader.loadClass("com.wxueyuan.classloader.ClassLoaderTest").newInstance();
		System.out.println(instance.getClass());
		System.out.println(obj.getClass());
		System.out.println(instance.getClass().equals(obj.getClass()));
		System.out.println(instance instanceof com.wxueyuan.classloader.ClassLoaderTest);
		System.out.println(obj instanceof com.wxueyuan.classloader.ClassLoaderTest);
		System.out.println(instance.getClass().getClassLoader());
		System.out.println(obj.getClass().getClassLoader());
	}
}

这段程序的运行结果如下:

class com.wxueyuan.classloader.ClassLoaderTest
class com.wxueyuan.classloader.ClassLoaderTest
false
true
false
sun.misc.Launcher$AppClassLoader@73d16e93
com.wxueyuan.classloader.ClassLoaderTest$1@7852e922

从前两句输出我们可以看出,instance 对象和 obj 对象都是 class com.wxueyuan.classloader.ClassLoaderTest 这个类的实例。

但是当我们尝试 instance.getClass().equals(obj.getClass())却得到了 false,这两个实例的类并不相等,这就是因为 ClassLoaderTest 这个类是由不同的类加载器加载的,换句话说在 Java 虚拟机中存在着两个不同的 ClassLoaderTest 类。

当我们分别用 instance 和 obj 对象进行 instanceof 操作时我们发现 instance 返回 true,而 obj 返回 false;这是因为默认的 com.wxueyuan.classloader.ClassLoaderTest 这个类是由系统应用程序类加载器加载的,而 obj 这个实例其实是我们用 myLoader 类加载器加载的类的实例。最后两行打印输出了这两个对象所属的不同的类加载器。

2. 类加载器的分类

Java 虚拟机的角度来看,类加载器可以分为两类:

  1. 启动类加载器(Bootstrap ClassLoader): 这个类加载器是由 C++ 实现的,是 Java 虚拟机的一部分。
  2. 其它类加载器: 除了启动类加载器,其它所有的加载器都是由 Java 实现的,独立于 Java 虚拟机外部,都继承自抽象类 Java.lang.ClassLoader。

Java 开发人员的角度来看,类加载器可以分为四类:

  1. 启动类加载器(Bootstrap ClassLoader): 这个类加载器主要负责加载 JAVA_HOME/lib 目录下,或者被-Xbootclasspath 参数所指定的路径下的,可以被虚拟机识别的指定格式的(如 rt.jar)类库。
  2. 扩展类加载器(Extension ClassLoader): 这个类加载器由 sun.misc.Launcher$ExtClassLoader 实现,它负责加载 JAVA_HOME/lib/ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
  3. 应用程序类加载器(Application ClassLoader): 这个类加载器由 sun.misc.LauncherAppClassLoader实现。这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以我们也称它为系统类加载器。它负责加载用户类路径上所指定的类库,如果应用程序没有自定义过类加载器,这个类加载器也是默认的类加载器,现在大家知道上面的例子当中为什么instance实例的getClassLoader()方法返回的是sun.misc.LauncherAppClassLoader@73d16e93 了吧。
  4. 自定义类加载器:通常情况下我们的应用程序就是由这三种类加载器配合加载的,但是在必要的情况下我们可以自定义类加载器,就比如我们上面的 myLoader。

3. 双亲委派模型

上面介绍的 4 中类加载器之间存在着一种层次关系,这种层次关系也被称为双亲委派模型(Parents Delegastion Model),如下图所示:

f9ab8f6cc4ee44879dff32c9c983a54a.jpg

双亲委派模型要求除了顶层的启动类加载器之外,其它所有的类加载器都必须有自己的父加载器。当一个类加载器收到了类加载的请求之后,它首先不会尝试自己加载这个类,而是将这个请求委派给父类加载器去完成,由于每一个类加载器都有这个逻辑,因此所有的类加载请求最终都应该传送到顶层的启动类加载器中。只有在父加载器在它的搜索范围内没有找到所需的类时,子加载器才会尝试去加载。

双亲委派模型对 Java 程序的稳定运行很重要,因为即使我们创建了与类库中全限定名相同的类,我们的类也不会被加载而影响到整个 Java 的运行环境。它的实现逻辑都集中在 java.lang.ClassLoader.loadClass()方法中

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) {
					  //调用父类加载器的loadClass
					  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();
				  //在父类加载器无法加载的时候,再调用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;
	  }
  }

4. 破坏双亲委派模型

尽管双亲委派模型对 Java 程序的运行十分重要,但是在一些情况下,我们不得不对双亲委派模型进行破坏。比如双亲委派模型能够保证越基础的类越由上层的加载器加载,基础的类库之所以基础,是因为它们提供了大量的用户需要调用的 API,但是在一些情况下基础的类库有可能要反过来调用用户提供的代码又该怎么办呢?

有的同学可能会说哪有基础类库需要调用用户提供的代码的时候,但事实上这种情况确实存在。比如 SUN 公司提供 JNDI 服务用来对命名服务或目录服务的资源进行管理和查找,它的代码是由启动类加载器去加载的(rt.jar),但是它需要调用由服务供应商提供的 SPI 的实现类(JNDI 相关知识请关注此处)。

为了解决这个问题,JAVA 设计团队提供了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过 java.lang.Thread 类的 setContextClassLoader()方法进行设置,如果创建线程时未设置,则会继承父线程的 ContextClassLoader。

有了线程上下文类加载器,JAVA 就可以实现父类加载器请求子类加载器去完成某些类的加载动作,这种行为相当于逆向使用了双亲委派模型的加载顺序。

5. 总结

在这篇博客中博主与大家一起学习了一下 Java 中的类加载机制,以及完成类加载动作的类加载器和传统的双亲委派模型,事实上,类加载的过程包括着“加载”,“验证”,“准备”,“解析”和“初始化”等 5 个阶段,博主在这里就不赘述了,感兴趣的同学可以自己下去查些资料去更深一步地了解类加载的过程。

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 201 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...