Java类加载器一

类加载器介绍

在前面已经有一篇笔记简单介绍了Java类加载机制,但是限于篇幅所以将类加载器单独新启一篇继续介绍。Java类加载器处于类加载机制的第一个阶段,如果在这个阶段对类文件做任何更改都将会对运行产生重大影响。由于类加载都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会给java应用程序提供高度的灵活性。

本文所有示例都是基于JDK1.8.0_111,虚拟机版本Java HotSpot(TM) 64-Bit Server VM。

Java类加载器Classloader作为Java运行时环境(Java Runtime Environment)的一部分,负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。每个Java类必须由某个类加载器装入到内存。

JVM中有3个默认的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。一般是C++实现的,它负责加载Java的基础类,主要是/lib/rt.jar,我们日常用的Java类库比如String, ArrayList等都位于该包内。
  • 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Launcher$ExtClassLoader,它负责加载Java的一些扩展类,一般是/lib/ext目录中的jar包。
  • 应用程序类加载器(Application ClassLoader):该加载器有时也被称作系统类加载器,因为通过 ClassLoader.getSystemClassLoader()来获取的就是该类加载器。应用程序类加载器的实现类是sun.misc.Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。

类加载器之间关系

在探讨各个类加载器之间关系之前,下面先来看一个示例。首先新建一个空的JavaBean User类,然后通过User类来看一下上述三种类加载器的搜索路径。

ClassLoader classLoader01=User.class.getClassLoader();
System.out.println(classLoader01);

ClassLoader classLoader02=classLoader01.getParent();
System.out.println(classLoader02);

System.out.println(classLoader02.getParent());
//sun.misc.Launcher$AppClassLoader@73d16e93
//sun.misc.Launcher$ExtClassLoader@15db9742
//null

由于引导类加载器不是使用Java语言实现的,所以这里打印结果是null。我们直接获取到User类的类加载器是AppClassLoader,然后通过getParent()方法再次获取就拿到了ExtClassLoader类加载器,所以在这里我们可以说这两个类之间的关系是父子关系,但是却不能认为就是AppClassLoader继承自ExtClassLoader,事实上它们两个不是继承关系,这两个类都是ClassLoader的子类,而且它们两个是平级的并且都是继承自ClassLoader子类URLClassLoader。在URLClassLoader中有一个方法getURLs(),这个方法可以返回一个URL数组,我们可以通过这个方法查看一下类加载器路径。

private static void printUrl(URL[] urls){
	for(int i=0;i<urls.length;i++){
		System.out.println(urls[i]);
	}
}

AppClassLoader加载路径

首先来看一下的应用类加载器AppClassLoader加载路径位于哪里。

ClassLoader classLoader=User.class.getClassLoader();
URL[] urls=((URLClassLoader)classLoader).getURLs();
printUrl(urls);

打印出来的路径如下:

file:/D:/workspace/java/classloader/bin/
file:/D:/workspace/java/classloader/libs/commons-io-2.6.jar

这里我们的示例代码是运行在eclipse中的,所以这里类加载路径就是位于编译后的.class文件所在的路径以及第三方jar路径。类加载路径除了通过getURLs()方法之外还可以通过System.getProperty("java.class.path")的方式获取到。AppClassLoader的搜索路径是由java.class.path来指定的。

ExtClassLoader的类加载路径

按照类似于AppClassLoader处理方式打印ExtClassLoader的类加载路径如下:

file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/access-bridge-64.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/cldrdata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/dnsns.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/jaccess.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/jfxrt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/localedata.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/nashorn.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/sunec.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/sunjce_provider.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/sunmscapi.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/sunpkcs11.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/ext/zipfs.jar

当然了也可以使用System.getProperty("java.ext.dirs")方式获取到加载的搜索路径。ExtClassLoader类加载的搜索路径位于<JAVA_HOME>/lib/ext目录。

引导类加载器加载路径

由于引导类加载器是使用非Java语言编写的,如果这使用上面的按照类的获取方式是不可行的,所以我们直接使用System.getProperty("sun.boot.class.path")方式获取,结果输出如下:

file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_111/jre/classes

引导类加载器加载的是<JAVA_HOME>/lib目录下的部分jar包,当然了我们也可以使用如下方式获取类加载的jar包。

URL[] urls=sun.misc.Launcher.getBootstrapClassPath().getURLs();
printUrl(urls);

双亲委托机制

上文三种类加载器为父子关系,AppClassLoader的父亲是Extension ClassLoader,ExtClassLoader的父亲是Bootstrap ClassLoader,注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子ClassLoader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:

  1. 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个ClassLoader加载一次。
  2. 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
  3. 在父ClassLoader没有加载成功的前提下,自己尝试加载类。

这个过程一般被称为"双亲委派"模型,即优先让父ClassLoader去加载。为什么要先让父ClassLoader去加载呢?这样,可以避免Java类库被覆盖的问题,比如用户程序也定义了一个类java.lang.String,通过双亲委派,java.lang.String只会被Bootstrap ClassLoader加载,避免自定义的String覆盖Java类库的定义。

Java为什么要采用这样的委托机制?理解这个问题,我们引入另外一个关于Classloader的概念“命名空间”, 它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说即使两个类的全限定名是相同的,但是因为不同的ClassLoader加载了此类,那么在JVM中它是不同的类。明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面说的,我们JDK本生提供的类库,比如String、ArrayList等等,这些类由Bootstrap 类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。另外一个方面也是为了安全,如果某些开发者认为系统类提供不了自己需要的功能,而是自己写了一个系统类,如String类,下面是一个简单的示例。

package java.lang;

public class String {
	
	...
	public String toString() {
		return "Hello World";
	}

}

这里我们自己重写了一个String类,而且包名也是跟系统内置的String类相同的,但是这里我们将String的toString()方法重写了,如果JVM中类加载不是双亲委托,这时候就会直接调用我们自己的String类了,那么项目中所有的toString()方法就全部被重写了,使用同样的方式也可以重写String中其它方法,这种后果简直是不可想象的。

下面是调用了String类之后的运行的结果。

String str="ni hao";
System.out.println(str.getClass().getClassLoader());
System.out.println(str.toString());
//null
//ni hao

从运行结果可以知晓,这里仍然调用了系统内置的String类,这就是双亲委托机制的好处。到这里可能就又有疑问了,既然是双亲委托机制,先加载<JAVA_HOME>/lib目录下的类,然后加载<JAVA_HOME>/lib/ext下的类,最后才加载自己写的应用类和引入的第三方法类库,那么我直接将自己写的类打包成jar包放到系统的lib目录下,这样仍然可以破坏安全性。实际上这种方式也是不可行的,虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。

理解ClassLoader

除了所说的双亲委托机制,也是按需加载,只有首次使用的时候才加载,JVM在加载时有如下三个特点:

全盘负责:全盘负责机制是说当一个类加载器负责加载某个类的时候,该类所依赖的和引用的其他类也将由该类加载器负责载入,除非显式使用另一个类加载器来载入; 父类委托:父类委托机制是说当一个类加载器负责加载某个类的时候,先让父类加载器尝试载入该类,若无法载入,才尝试从自己的类路径中载入; 缓存机制:缓存机制会保证所有被加载过的类都会被缓存。当一个类加载器负责加载某个类的时候,会先从缓存中搜寻该类,只有当缓存中不存在该类对象时,才会载入该类并存放于缓存中。这也是为什么我们修改了某个类以后需要重启动JVM,所做的调整才会生效的原因。

ClassLoader是一个抽象类,AppClassLoader和ExtClassLoader都是ClassLoader的子类,Bootstrap ClassLoader不是Java语言实现的,并没有对应的类。

ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码生成Class类的一个实例。除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如下所示:

方法说明
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 类。

如果要自定义一个ClassLoader,loadClass()尽量不要重写,而是重写findClass()方法即可,为什么要这么说,下面分析一下源码。

public Class<?> loadClass(String name) throws ClassNotFoundException {
	return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
{
	synchronized (getClassLoadingLock(name)) {
		// 首先,检查类是否已经被加载了
		Class<?> c = findLoadedClass(name);
		if (c == null) {
			try {
				//委派父ClassLoader,resolve参数固定为false
				if (parent != null) {
					c = parent.loadClass(name, false);
				} else {
					//如果仍然找到则直接使用引导类加载器
					c = findBootstrapClassOrNull(name);
				}
			} catch (ClassNotFoundException e) {
				//没找到,捕获异常,以便尝试自己加载       
			}

			if (c == null) {
				// If still not found, then invoke findClass in order
				// to find the class.
				// 自己去加载,findClass才是当前ClassLoader的真正加载方法
				c = findClass(name);
				...
			}
		}
		// 链接,执行static语句块
		if (resolve) {
			resolveClass(c);
		}
		return c;
	}
}

类加载器加载类大致要经过8个步骤:

  1. 首先调用findLoadedClass()方法,检测目标类是否有载入过(即在缓存中是否有这个类),如果有,则直接进入第八步;
  2. 如果父加载器parent不存在(父加载器不存在有两种情形,一是当前加载器的parent是根加载器,二是当前加载器就是根加载器),则跳到第四步执行;如果父加载器存在,则执行第三步;
  3. 请求父加载器调用loadClass()方法载入目标类,如果成功跳到第八步,不成功则跳到第五步;
  4. 请求使用根加载器的findBootstrapClassOrNull()方法加载目标类,如果成功跳到第八步,不成功则跳到第七步;
  5. 调用findClass()方法寻找class文件(从相关的类加载器的加载路径中查找),如果找到则执行第六部,找不到则执行第七步;
  6. 从文件中载入类,成功载入则跳到第八步;
  7. 抛出ClassNotFoundException;
  8. 返回一个Class实例。

这里就是所谓的双亲委托机制,这种机制是Java推荐的机制,但是却并不是Java的强制机制,所以暴露出来了ClassLoader的部分方法方面开发者自定义自己的类加载器。

protected Class<?> findClass(String name) throws ClassNotFoundException {
	throw new ClassNotFoundException(name);
}

在ClassLoader中可以看到findClass()直接抛出了一个异常,这个是有一个历史原因的,因为双亲委派模型是JDK1.2以后才引用进来的,在1.1及以前用户实现自己的类加载器都是通过重写loadClass方法实现,为了兼容原来的实现方式,就选择了增加findClass这么一种妥协的方式。

通过上面分析可以知道,因为JDK已经在loadClass()方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass()方法中搜索不到类时,loadClass()方法就会调用findClass()方法来搜索类,所以我们只需重写该方法即可。

类加载的过程

由于类加载是双亲委托机制,所以真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass()来实现的,该方法是final修饰符修饰的方法,子类不可重写;而启动类的加载过程是通过调用loadClass()来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

方法loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法defineClass()抛出的是 java.lang.NoClassDefFoundError异常。在自定义类加载器的时候一般都是通过findClass()方法和defineClass实现的。

其它

本篇重点在于介绍ClassLoader的概念,双亲委托机制Java推荐的类加载机制,当然了也有打破这种机制的类加载器,而且应用也非常广泛,最常见的就是Apache Tomcat服务器。ClassLoader是实现Java动态扩展的实现方式之一,实际上还有一种方式就是通过java.lang.Class的forName()方法,在下一篇文章中我们再类比ClassLoader介绍,包括如果自定义ClassLoader,当然了还有一个线程上下文类加载器。

System.out.println(ArrayList.class.getClassLoader());//null
System.out.println(int.class.getClassLoader());//null
System.out.println(ClassLoader.getSystemClassLoader());//sun.misc.Launcher$AppClassLoader@73d16e93
System.out.println(User.class.getClassLoader());//sun.misc.Launcher$AppClassLoader@73d16e93
System.out.println(User.class.getClassLoader().getParent());//sun.misc.Launcher$ExtClassLoader@15db9742

int[] intArray=new int[10];
System.out.println(intArray.getClass().getClassLoader());//null

User[] users=new User[10];
System.out.println(users.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@73d16e93

数组类的Class对象不是由类加载器创建的,而是由Java运行时根据需要自动创建。数组类的类加载器由Class.getClassLoader()返回,该加载器与其元素类型的类加载器是相同的;如果该元素类型是基本类型,则该数组使用的是引导类加载器。

参考资料

Java类加载器

深入探讨 Java 类加载器

深入探讨 Java 类加载器

深入理解Java类加载器(1):Java类加载原理解析

java 类加载器

Java类加载器ClassLoader总结

类加载机制:全盘负责和双亲委托

http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/sun/misc/Launcher.java

评论

您确定要删除吗?删除之后不可恢复