类加载器

7.4 类加载器

类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节流”这个动作在Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为“类加载器”(Class Loader)。

7.4.1 类与类加载器

对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。

既,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,

否则,即使这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类必定不相等。

这里的”相等”是指,包括代表类的Class对象的equals方法、isAssignableFrom()方法、isIntance()方法的返回结果,也包括了使用InstanceOf关键字做对象所属关系判定等情况。

7.4.2 双亲委托模型

从JAVA虚拟机来看,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器使用C++语言实现,是虚拟机自身的一部分
  • 其他所有的类加载器:由Java实现,独立在虚拟机外部,全继承于java.lang.ClassLoader

在JDK8以及之前版本的Java,主要使用以下3个系统提供的类加载器来进行加载:

  • 启动类加载器(Bootstrap Class Loader):其负责加载<JAVA_HOME>\lib目录,或者其他能被Java虚拟机识别的类库加载到虚拟机的内存中
  • 扩展类加载器(Extension Class Loader):其负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。由于扩展类加载器是由Java代码实现的,开发者可以直接在程序中使用扩展类加载器来加载class文件
  • 应用程序类加载器(Application Class Loader):其负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义自己的类加载器,一般情况下就是程序中默认的类加载器。

JDK9之前的java应用都是由这三种类加载器互相配合完成加载的,若有必要,还可以加入自定义的类加载器来拓展,可以有以下方式:

  • 增加除磁盘位置之外的class文件来源
  • 通过类加载器实现类的隔离重载等功能,如下图所示。

image-20210604201615531

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。

这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,通常使用组合(Composition)关系来复用父加载器的代码。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去完成加载。

使用双亲委派模型有一个显而易见的好处,就是Java中的类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能保证是同一个类。

双亲委派模型的实现如下:

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
protected synchronized Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException
{
// 首先,检查请求的类是否被加载
Class c = findLoadedClass(name);

if(c==null){
try{
if(parent != null){
c = parent.loadClass(name, false);
}else{
c = findBootstrapClassOrNull(name);
}
}
catch(ClassNotFoundExcepton e){
// 若父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if(c==null){
// 在父类加载器无法加载时,
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if(resolve){
resolveClass(c);
}
return c;
}

其先检查请求加载的类型是否被加载过,若没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。

既然父加载器加载失败,抛出ClassNotFoundException异常,才调用自己的findClass()方法进行尝试加载。

7.4.3 破坏双亲委派模型

双亲委派模型第三次”被破坏“是由于用户对程序动态性的追求而导致的,如代码热替换,模块热部署等。

OSGi实现模块热部署的关键是它自定义的类加载器机制的实现,每一个程序模块都有一个自己的类加载器,当需要更换一个模块时,就模块连同类加载器一起换掉以实现代码的热替换。

在OSGi环境下,类加载器不再双亲委派模型推荐的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi将按照下面的顺序进行搜索:

  1. 将以java.*开头的类,委派给父类加载器加载
  2. 否则,将委派列表名单内的类,委派给父类加载器加载
  3. 否则,将Import列表中的类,委派给Export这个类的Bundle的类加载器加载
  4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载
  5. 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载
  6. 否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载
  7. 否则,查找失败

以上查找顺顺序,只有开头两点仍符合双亲委派模型的原则,其余类查找都是在平级的类加载器中进行。


9.2.1 Tomcat:类加载器架构

在Tomcat目录中,可以设置3组目录存放Java类库,分别是(/common/*/server/*/shared/*),还有Web程序自身的/WEB-INF/*目录,这4组目录每一组都有特别的含义,分别是:

  • /common目录中,类库可被tomcat和所有的web程序共同使用
  • /server目录中,类库可被Tomcat使用,对所有的Web应用程序不可见
  • /shared目录中,类库可被所有的Web程序使用,但对Tomcat自己不可见。
  • /WebApp/WEB-INF中,类库仅可被该Web程序使用,对TOmcat和其他应用不可见

其加载器关系如下图所示:

image-20210604211319845

从图中可得,Common类加载器能加载的类都可以被Catalina类加载器和Shared类加载器使用,而Cataline类加载器和Shared类加载器自己能加载的类则与对方相互隔离。

WebApp类加载器可以使用Shared类加载器加载到的类,但各个WebAPp加载器之间相互隔离。而JasperLoader的加载范围仅是这个JSP文件所编译出来的那一个Class文件,它存在的目的就是为了被丢弃:当服务器检测到JSP文件被修改时,会替换掉目前JasperLoader的实例,并通过再建立一个新的JSP类加载器来实现JSP文件的HotSwap功能。

作者

bd160jbgm

发布于

2021-06-04

更新于

2021-06-04

许可协议