8、类加载器与双亲委派模型

显示加载

  • 当 JVM 执行第一行代码“ Student s = new Student();”时
  • JVM 先碰到了 Student 类,“ Student s = new Student();”
  • 此时,JVM 将查看方法区中是否有 Student 对应的 Class 对象(我们学习过反射,都知道 Class 对象,在同一个 JVM 中,可以有很多的 Student 实例,但是 Student 的 Class 对象只有一个)。
  • 如果加载过了,那么直接返回
  • 因为是第一次执行,方法区中没有 Student 的 Class 对象,此时 JVM 就会调用类加载器(ClassLoader)。
  • ClassLoader会使用父类加载器,父类加载器如果加载过了直接返回,没有就调用父类的父类。直到没有父类加载器,他会判断自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载。

1.作用

  1. 实现类加载的功能
  2. 确定被加载类在Java虚拟机中的唯一性

1.1 实现类加载的功能

即实现类加载过程中“加载”环节里“通过类的全限定名来获取定义此类的二进制字节流”的功能(类加载5个阶段)

1.2 确立被加载类在Java虚拟机中的唯一性

  • 确定两个类是否相等的依据:是否由同一个类加载器加载
  1. 若由同一个类加载器加载,则这两个类相等; 若由不同的类加载器加载,则这两个类不相等。
  2. 即使两个类来源于同一个 Class 文件、被同一个虚拟机加载,这两个类都不相等
  • 在实际使用中,是通过下面方法的返回结果(Boolean值)进行判断:
  1. Class对象的equals()方法
  2. Class对象的isAssignableFrom()方法
  3. Class对象的isInstance()方法
  • 当然也会使用instanceof关键字做对象所属关系判定等情况
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
51
52
53
54
55
56
57
58
/例子
public class Test {

// 自定义一个类加载器:myLoader
// 作用:可加载与自己在同一路径下的Class文件
static ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {

if (!name.equals("com.carson.Test"))
return super.loadClass(name);

try {
String fileName = name.substring(name.lastIndexOf(".") + 1)
+ ".class";

InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(fileName);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);

} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};

public static void main(String[] args) throws Exception {

Object obj = myLoader.loadClass("com.carson.Test");
// 1. 使用该自定义类加载器加载一个名为com.carson.Test的类
// 2. 实例化该对象

System.out.println(obj);
// 输出该对象的类 ->>第一行结果分析

System.out.println(obj instanceof com.carson.Test);
// 判断该对象是否属于com.carson.Test类 ->>第二行结果分析

}

}

<-- 输出结果 -->
class com.carson.Test
false

// 第一行结果分析
// obj对象确实是com.carson.Test类实例化出来的对象

// 第二行结果分析
// obj对象与类com.huachao.Test做所属类型检查时却返回了false
// 原因:虚拟机中存在了两个Test类(1 & 2):1是由系统应用程序类加载器加载的,2是由我们自定义的类加载器加载
// 虽然都是来自同一个class文件,但由于由不同类加载器加载,所以依然是两个独立的类
// 做对象所属类型检查结果自然为false。

重点为:

  1. 启动类加载器
  2. 扩展类加载器
  3. 应用程序类加载器

2.1启动类加载器(Bootstrap ClassLoader)

  • 作用:负责加载以下类
  1. 存放在 < JAVA_HOME > \ lib中目录中的类
  2. 被-Xbootclasspath参数所指定路径中,并且是被虚拟机识别的类库

仅按文件名识别,如:rt.jar中,名字不符合的类库即使放在LIB目录中也不会被加载

  • 特别注意
  1. 启动类加载器无法被Java程序直接引用
  2. 用户在编写自定义类加载器时,若需把加载请求委派给引导类加载器,直接使用null代替即可,如java.lang.ClassLoader.getClassLoader()方法所示:
1
2
3
4
5
6
7
8
9
10
11
@CallerSensitive 
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}

2.2扩展类加载器(Extension ClassLoader)

  • 作用:负责加载以下类:
  1. < JAVA_HOME > \ lib中\分机目录中的类库
  2. 被java.ext.dirs系统变量所指定的路径中的所有类库
  • 特别注意
  1. 由sum.misc.Launcher $的ExtClassLoader类实现
  2. 开发者可以直接使用扩展类加载器

2.3应用程序类加载器(Application ClassLoader)

  • 作用:

负责加载用户类路径(ClassPath)上所指定的类库

  • 特别注意
  1. 也称为系统类加载器,因为该类加载器是类加载器中的getSystemClassLoader()方法的返回值
  2. 由sum.misc.Launcher $ AppClassLoader类实现
  3. 开发者可以直接使用该类加载器
  4. 若开发者没自定义类加载器,程序默认使用该类加载器

各种类加载器的使用并不是孤立的,而是相互配合使用

在Java虚拟机中,各种类加载器配合使用的模型(关系)是双亲委派模型

3.双亲委派模型

作用:

  • 因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器再加载一次。避免同样多份字节码加载
  • 安全
  • 采用双亲委派的一个好处是比如加载位于rt.jar的包中的类java.lang.Object中,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个目标对象。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。因此,使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处:类随着它的类加载器一起具备了一种带有优先级的层次关系。

双亲委派模型的工作流程代码实现在 java.lang.ClassLoader中的的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
//具体如下
@Override
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);

// 检查需要加载的类是否已经被加载过
if (c == null) {
try {
// 若没有加载,则调用父加载器的loadClass()方法
if (parent != null) {
c = parent.loadClass(name, false);
}else{
// 若父类加载器为空,则默认使用启动类加载器作为父加载器
c=findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 若父类加载器加载失败会抛出ClassNotFoundException,
//说明父类加载器无法完成加载请求
}
if(c==null){
// 在父类加载器无法加载时
// 再调用本身的findClass方法进行类加载
c=findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}

各个类加载器之间是组合关系,并非继承关系。

步骤总结:若一个类加载器收到了类加载请求

  1. 该类把加载请求委派给父类加载器去完成,而不会自己去加载该类:每层的类加载器都是如此,因此所有的加载请求最终都应传送到顶层的启动类加载器中

  2. 只有当父类加载器反馈自己无法完成该加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会自己去加载

以自定义STRIG类为例子

加载某个类时,优先使用父类加载器加载需要使用的类。如果我们自定义了java.lang.String这个类,加载该自定义的字符串类,该自定义字符串类使用的加载器是AppClassLoader ExtClassLoader,所有这些加载字符串使用的类加载器是ExtClassLoader,但是类加载器ExtClassLoader在jre / lib / ext目录下没有找到String.class类。然后使用ExtClassLoader父类的加载器BootStrap,父类加载器BootStrap在JRE / lib目录的rt.jar找到了String.class,将其加载到内存中。这就是类加载器的委托机制。

优点
Java的类随着它的类加载器一起具备了一种带优先级的层次关系

如:类java.lang.Object(存放在rt.jar中)在加载过程中,无论哪一个类加载器要加载这个类,最终需委派给模型顶端的启动类加载器进行加载,因此对象类在程序的各种类加载器环境中都是同一个类。

若没有使用双亲委派模型(即由各个类加载器自行去加载),用户编写了一个java.lang.Object继承的类(放在类路径中),那系统中将出现多个不同的对象类,Java的体系中最基础的行为就无法保证。

4.自定义类加载器

以下代码中的FileSystemClassLoader是自定义类加载器,继承自java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class文件),然后读取该文件内容,最后通过defineClass()方法来把这些字节代码转换成java.lang.Class类的实例。

java.lang.ClassLoader的loadClass()实现了双亲委派模型的逻辑,自定义类加载器一般不去重写它,但是需要重写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
40
public class FileSystemClassLoader extends ClassLoader {

private String rootDir;

public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}

protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}

private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
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
//主要是通过继承自ClassLoader类 从而自定义一个类加载器 MyClassLoader.java
// 继承自ClassLoader类
public class MyClassLoader extends ClassLoader {
// 类加载器的名称
private String name;
// 类存放的路径
private String classpath = "E:/";

MyClassLoader(String name) {
this.name = name;
}

MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}

@Override
public Class<?> findClass(String name) {
byte[] data = loadClassData(name);
return this.defineClass(name, data, 0, data.length);
}

public byte[] loadClassData(String name) {
try {
name = name.replace(".", "//");
System.out.println(name);
FileInputStream is = new FileInputStream(new File(classpath + name
+ ".class"));
byte[] data = new byte[is.available()];
is.read(data);
is.close();
return data;

} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

假如我们的类不在类路径下,而我们又想读取一个自定义的目录下的类,如果做呢?
//示例读取c:/test/com/test.jdk/Key.class这个类。

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
package com.test.jdk;

public class Key {
private String key = "111111";
}
import org.apache.commons.io.IOUtils;

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

public class LocalClassLoader extends ClassLoader {

private String path = "c:/test/";

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> cls = findLoadedClass(name);
if (cls != null) {
return cls;
}

if (!name.endsWith(".Key")) {
return super.loadClass(name);
}

try {
InputStream is = new FileInputStream(path + name.replace(".", "/") + ".class");
byte[] bytes = IOUtils.toByteArray(is);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
}

return super.loadClass(name);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
//自定义类加载器正常加载到类,程序最后输出:111111
public static void main(String[] args) {
try {
LocalClassLoader lcl = new LocalClassLoader();
Class<?> cls = lcl.loadClass("com.test.jdk.Key");
Field field = FieldUtils.getField(cls, "key", true);
Object value = field.get(cls.newInstance());
System.out.println(value);
} catch (Exception e) {
e.printStackTrace();
}
}

URLClassLoader上面自定义一个类加载器来读取自定义的目录,其实可以直接使用URLClassLoader就能读取,它已经实现了路径下类的读取逻辑

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
try {
URLClassLoader ucl = new URLClassLoader(new URL[]{new URL("c:/test/")});
Class<?> cls = ucl.loadClass("com.test.jdk.Key");
Field field = FieldUtils.getField(cls, "key", true);
Object value = field.get(cls.newInstance());
System.out.println(value);
} catch (Exception e) {
e.printStackTrace();
}
}

5.破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型。

双亲委派模型主要出现过3次较大规模“被破坏”的情况。

  1. 第一次破坏是因为类加载器和抽象类java.lang.ClassLoader中在JDK1.0就存在的,而双亲委派模型在JDK1.2之后才被引入,为了兼容已经存在的用户自定义类加载器,引入双亲委派模型时做了一定的妥协:在java.lang.ClassLoader中中引入了一个的findClass()方法,在此之前,用户去继承java.lang.ClassLoader中的唯一目的就是重写的loadClass()方法.JDK1 0.2之后不提倡用户去覆盖的loadClass()方法,而是把自己的类加载逻辑写到的findClass()方法中,如果的loadClass()方法中如果父类加载失败,则会调用自己的的findClass()方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派模型规则的。

  2. 第二次破坏是因为模型自身的缺陷,现实中存在这样的场景:基础的类加载器需要求调用用户的代码,而基础的类加载器可能不认识用户的代码为此,Java的设计团队引入的设计时“线程上下文类加载器(Thread Context ClassLoader)”。这样可以通过父类加载器请求子类加载器去完成类加载动作。已经违背了双亲委派模型的一般性原则。

  3. 第三次破坏是由于用户对程序动态性的追求导致的。这里所述的动态性是指:“代码热替换”,“模块热部署”等等比较热门的词。说白了就是希望应用程序能够像我们的计算机外设一样,接上鼠标,U盘不用重启机器就能立即使用.OSGi是当前业界“事实上”的Java的模块化标准,OSGi的实现模块化热部署的关键是它自定义的类加载器机制的实现。每一个程序模块(OSGi的中称为包)都有一个自己的类加载器,当需要更换一个包时,就把束连同类加载器一起换掉以实现代码的热替换。在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构。当收到类加载请求时,osgi将按照下面的顺序进行类搜索:

  • 1)将以java。*开头的类委派给父类加载器加载
  • 2)否则,将委员列表名单(配置文件org.osgi.framework.bootdelegation中定义)内的类委派给父类加载器加载
  • 3)否则,检查是否在Import-Package中声明,如果是,则委派给出口这个类的Bundle的类加载器加载
  • 4)否则,检查是否在Require-Bundle中声明,如果是,则将类加载请求委托给必需的捆绑的类加载器
  • 5)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  • 6)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
  • 7)否则,查找Dynamic Import-Package(动态导入只有在真正用到此包的时候才进行加载)的Bundle,委派给对应Bundle的类加载器加载
  • 8)否则,类查找失败

隐式加载

他们都能在运行时对任意一个类,都能够知道该类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。

  • 的loadClass

加载

链接(校验准备解析)

初始化

Classloder.loaderClass得到的类是还没有链接的

  • 的forName

的Class.forName得到的类是已经初始化完成的

调用的时候静代码块直接运行

这样我们需要初始化后才能得到的DriverManager,所以我们选择使用的Class.forName()