类加载器,双亲委派模型java9之前之后的区别

Posted by hang.li on November 20, 2022

jdk8以及以前的类的加载机制,主要依托双亲委派模型对类进行加载

  1. 启动类加载器 负责加载目录(\lib)下或者被-Xbootclasspath参数所指定的路径中存放的, 制定包名文件名的类(jdk内置的包名)。该加载器属于jvm,使用c语言编写,如果使用该加载器的类获取其加载器会出现null情况,如: ![img.png](/img/in-post/java/see-classloader.png)

  2. 扩展类加载器 负责加载载\lib\ext目录中,或者被java.ext.dirs系统变量所 指定的路径中所有的类库。 该加载器使用java代码编写类加载器是在类sun.misc.Launcher$ExtClassLoader。

3.应用类加载器 负责加载用户类路径 (ClassPath)上所有的类库,默认的类加载器,使用java编写,sun.misc.Launcher$AppClassLoader来实现。

  1. 双亲委派机制(java8及其以前) 直接上代码,jdk8中java.lang.ClassLoader#loadClass(java.lang.String, boolean)方法
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
// 对类的加载过程加锁,保证一个类在多个线程情况下只能被加载一次
synchronized (getClassLoadingLock(name)) {
	// 使用native方法判断该类是否在虚拟机中已经被加载了
	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) {
			// 在此过程中报错了,捕获到,说明父类加载器没有成功加载该类
		}

		// 父类加载器没有成功加载该类,由当前类加载器去加载
		if (c == null) {
			// 记录自己加载当前类的开始时间
			long t1 = System.nanoTime();
			// 使用当前类加载器进行加载
			c = findClass(name);

			// 记录类的加载状态
			sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
			sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
			sun.misc.PerfCounter.getFindClasses().increment();
		}
	}
	// 是否解析当前类
	if (resolve) {
		resolveClass(c);
	}
	// 将加载的类进行返回
	return c;
}

img.png

  1. jdk9及其以上类的加载(并非严格意义上的双亲委派) 由于java9开始引入模块化编程的概念查看jdk.internal.loader.BuiltinClassLoader#loadClass方法。 当请求加载一个类时,先加载这个类将类名映射到它的包名。如果有一个模块定义为BuiltinClassLoader包含这个包,然后类加载器委托直接指向类装入器。如果没有模块包含此包, 然后它委托搜索父类加载器,如果不是在父类中找到,然后搜索类路径。
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
 synchronized (getClassLoadingLock(cn)) {
	// 检查当前类是否被jvm加载
	Class<?> c = findLoadedClass(cn);

	if (c == null) {

		// 寻找能够加载此包的对应已经加载模块
		LoadedModule loadedModule = findLoadedModule(cn);
		if (loadedModule != null) {

			// 寻找能够加载此包的对应已经加载模块,拿到该模块的加载器
			BuiltinClassLoader loader = loadedModule.loader();
			// 类的所属模块为当前模块,直接加载该类
			if (loader == this) {
				// 虚拟机是否开启模块化模式(目前还不太懂,感觉没啥用)
				if (VM.isModuleSystemInited()) {
					c = findClassInModuleOrNull(loadedModule, cn);
				}
			} else {
				// 否则调用对应的模块加载器进行加载该类
				c = loader.loadClassOrNull(cn);
			}

		} else {

			// 没有找到该包对应的模块加载器,使用父类的加载器
			if (parent != null) {
				c = parent.loadClassOrNull(cn);
			}

			// 父加载器没有成功加载,并且有与这个类装入器关联的类路径,尝试用当前类加载器加载(此处的加载方式和当前模块加载器加载调用方法不同)
			if (c == null && hasClassPath() && VM.isModuleSystemInited()) {
				c = findClassOnClassPathOrNull(cn);
			}
		}

	}

	// 解析该类
	if (resolve && c != null)
		resolveClass(c);

	return c;
}

此时类的加载可变成 img.png 6.打破双亲委派机制的方式 1)因为双亲委派机制的逻辑就写在java.lang.ClassLoader#loadClass(java.lang.String, boolean)方法中,可以自定义类加载器直接重写掉loadClass方法。 2) 启动类加载器只能加载指定包(路径)下的类,比方说jdbc中,jdk定义了好多规范在java.sql包中,并且各大应用厂商对其规范进行了相应的实现,个厂商的实现只能使用应用类加载器获取自定义加载器去实现,而java.sql中的类不能拿到这些下层的类加载器,此时就会使用,线程上下文类加载器 (Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方 法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内 都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 3)模块化OSGi或java9中的模块化