概述

ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。

类加载分类

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式

  • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用class.forName(name)this.getclass().getclassLoader().loadclass()加载class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

命名空间

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。

每一个类加载器,都拥有一个独立的类名称空间:

比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成
  • 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

类加载机制基本特征

  • 双亲委派模型:不是所有类加载都遵守这个模型

  • 可见性:子类加载器可以访问父加载器加载的类型,但是反过来是不允许的

  • 单一性:由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见

类加载器分类

JVM支持两种类型的类加载器,分别为引导类加载器和自定义类加载器。JVM规范将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器

  • 除顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。
  • 不同类加载器实际是包含关系。在下层加载器中,包含着上层加载器的引用

启动类加载器

  • 该类加载使用C/C++语言实现的,嵌套在JVM内部
  • 用于加载Java的核心库,用于提供JVM自身需要的类
  • 并不继承自java.lang.ClassLoader,没有父加载器
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器

扩展类加载器

  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 继承于classLoader类
  • 父类加载器为启动类加载器
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器

  • java语言编写,由sun.misc.Launcher$AppclassLoader实现
  • 继承于ClassLoader
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 它是用户自定义类加载器的默认父加载器
  • 通过ClassLoadergetSystemclassLoader()方法可以获取到该类加载器

用户自定义类加载器

  • Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源
  • 类加载器为应用程序提供一种动态增加新功能的机制,无须重新打包发布应用程序就能实现
  • 同自定义加载器能够实现应用隔离,例如Tomcat,Spring等中间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块
  • 自定义类加载器通常需要继承于ClassLoader

不同的类加载器

每个Class对象都会包含一个定义它的ClassLoader的一个引用

获取ClassLoader

方法 说明
clazz.getClassLoader() 获取当前的ClassLoader
Thread.currentThread().getContextClassLoader() 获取当前线程上下文的ClassLoader
ClassLoader.getSystemClassLoader() 获取系统的ClassLoader(应用程序类加载器)

说明

引导类加载器使用C++语言编写而成,而另外两种类加载器则是使用Java语言编写而成。由于引导类加载器不是一个Java类,因此在Java程序中只能打印出空值。

数组的类加载

数组类的Class对象不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。

对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的。如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。

ClassLoader

ClassLoader的主要方法

方法 说明
public final ClassLoader getParent() 返回该类加载器的超类加载器
public Class<?> loadClass(String name) throws ClassNotFoundException 加载名称为name的类,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。
protected Class<?> findClass(String name) throws ClassNotFoundException 查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVW鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) 根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
protected final void resolveClass(Class<?> c) 链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析
protected final Class<?> findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回结果为java.lang.Class类的实例。这个方法是final方法,无法被修改。

SecureClassLoader与URLClassLoader

SecureClassLoader

SecureClassLoader扩展了ClassLoader,新增与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法

URLClassLoader

ClassLoader是一个抽象类,很方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。

并新增URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这可以避免自己编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

ExtClassLoader和AppClassLoader

ExtClassLoader并没有重写loadClass()方法,说明其遵循双亲委派模式。而AppClaseLoader重载了loadClass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式。

Class.forName()与ClassLoader.loadClass()

方法 说明
Class.forName() 它是静态方法,常用Class.forName(String className),根据从传入的类全限定名返回Class对象。该方法将Class文件加载到内存的同时,会执行类的初始化
ClassLoader.loadClass() 它是实例方法,需要一个ClassLoader对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类的初始化,直到该类第一次使用时才进行初始化

双亲委派模型

定义与本质

定义

如果一个类加载器在收到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

本质

规定类加载的顺序:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

优势与劣势

双亲委派机制优势

  • 避免类的重复加载,确保一个类的全局唯一性
    Java类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载该类时,就没有必要子classLoader再加载一次。
  • 保护程序安全,防止核心API被随意篡改

代码支持

栗子

思考

双亲委派机制弊端

检查类是否加载的委托过程是单向的,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader无法访问底层的ClassLoader所加载的类

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。

比如在系统类中提供一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

沙箱安全机制

沙箱是一个限制程序运行的环境,Java安全模型的核心就是Java沙箱

  • 保证程序安全
  • 保护Java原生的JDK代码

沙箱机制就是将Java代码限定在虚拟机特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。

JDK 1.0

在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。

对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱机制

JDK 1.1

JDK1.0 中严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时就无法实现。因此Java1.1中针对安全机制做了改进,增加安全策略。允许用户指定代码对本地资源的访问权限

JDK 1.2

在Java1.2版本中再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。

JDK 1.6

当前最新的安全机制实现,则引入的概念。虚拟机会把所有代码加载到不同的系统域和应用域。

系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域对应不一样的权限。存在于不同域中的类文件就具有当前域的全部权限

自定义类的加载器

WHY

隔离加载类

在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如Tomcat这类web应用服务器,内部自定义了好几种类加载器,用于隔离同一个web应用服务器上的不同应用程序。

修改类加载方式

类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点进行按需进行动态加载

扩展加载源

比如从数据库、网络进行加载

防止源码泄漏

Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

常见场景

  • 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。
  • 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。

在一般情况下,使用不同的类加载器去加载不同的功能模块会提高应用程序的安全性。在做Java类型转换时,只有两个类型都是由同一个加载器所加载才能进行类型转换,否则转换时会发生异常。

实现方式

用户通过定制自己的类加载器,这样可以重新定义类的加载规则,以便实现一些自定义的处理逻辑。

实现方式

  • Java提供抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader
  • 在自定义ClassLoader的子类时,常见的两种做法:
    • 方式一:重写loadClass()方法
    • 方式二:重写findClass()方法

对比

建议做法是在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用:

  • loadClass()方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时也避免自己重写loadClass()方法过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
  • 当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。

说明

  • 其父类加载器是系统类加载器(应用程序类加载器)
  • JVM中的所有类加载都会使用java.lang.ClassLoader.loadClass(String)接口(自定义类加教器并重写java.lang.ClassLoader.loadClass(String)接口的除外),连JDK的核心类库也不能例外。

栗子

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
59
60
61
62
63
public class MyClassLoader extends ClassLoader {

private String byteCodePath;

public MyClassLoader(String byteCodePath) {
this.byteCodePath = byteCodePath;
}

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

@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {

BufferedInputStream bis = null;
ByteArrayOutputStream baos = null;

try {

int len;
byte[] data = new byte[1024];
// 字节码文件完整路径
String fileName = byteCodePath + className + ".class";
// 输入流
bis = new BufferedInputStream(new FileInputStream(fileName));
// 输出流
baos = new ByteArrayOutputStream();

// 读入数据并写出逻辑
while ((len = bis.read(data)) != -1) {
baos.write(data, 0, len);
}
// 获取内存中完整的字节数组的数据
byte[] byteCodes = baos.toByteArray();
// 将字节数组数据转换成Class实例
Class<?> clazz = defineClass(null, byteCodes, 0, byteCodes.length);
return clazz;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}

try {
if (bis != null) {
bis.close();
}
} catch (IOException e) {
e.printStackTrace();
}

}
return null;

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyClassLoaderTest {

public static void main(String[] args) {
MyClassLoader loader = new MyClassLoader("F:/");

try {
Class clazz = loader.loadClass("NewTest");
System.out.println("加载此类加载器为:" + clazz.getClassLoader().getClass().getName());
System.out.println("加载当前类的类加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}
}