概述
这是一个很常见的题目。今天我们就接着这个问题解析一下tomcat的类加载机制。全文会分成一下几个点来阐述,已经了解或者明白的某些概念的小伙伴们可以跳过对应的章节。
- 什么是类加载机制?
- 什么是双亲委派模型
- Tomcat的设计是如何破环双亲委派模型
- Tomcat的类加载器是如何设计
什么是类加载机制
对于一个java文件而言,他被运行的过程应该是这样的:
flowchart LR
E[用户定义的类]--编写-->A[java文件]--编译-->B[class文件]--加载-->C[JVM]--实例化-->D[具体对象]
在这个过程中,Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
类加载器
虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为“类加载器”。
类与类加载器的关系
对应Java虚拟机而言,任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。换句话说,如果这两个类来源于同一个Class文件,被同一个Java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。
什么是双亲委派机制
Java虚拟机中的内加载器
站在Java虚拟机的角度来看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另外一种就是其他所有的类加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader。
站在Java开发人员的角度来看,类加载器就应当划分得更细致一些。自JDK 1.2以来,Java一直保持着三层类加载器:
- 启动类加载器(Bootstrap Class Loader):加载存放在<JAVA_HOME>\lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中
- 扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Launcher$ExtClassLoader实现,它负责夹杂JAVA_HOME/lib/ext 目录下的,或者被java.ext.dirs 系统变量所指定的路径种的所有类库。开发者可以直接使用扩展类加载器。
- 这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
JDK 9之前的Java应用都是由这三种类加载器互相配合来完成加载的,如果用户认为有必要,还可以加入自定义的类加载器来进行拓展,典型的如增加除了磁盘位置之外的Class文件来源,或者通过类加载器实现类的隔离、重载等功能。
classDiagram
Bootstrap Class Loader <|-- Extension Class Loader
Extension Class Loader <|-- Application Class Loader
Application Class Loader <|-- User Class1 Loader
Application Class Loader <|-- User Class2 Loader
上图展示出来的各种类加载器之间的层次关系就被称为加载器的“双亲委派模型(Parents Delegation Model)”。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
使用双亲委派模型优点和缺点
优点
保证了核心类的加载是唯一的。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都能够保证是同一个类。确保唯一性的用途主要是为了保证类加载过程中的安全性问题。例如,我们可以使用安全管理器来限制某个类对某个路径的访问。如果有一个用户恶意的编写了一个名为java.lang.Object的类,因为JVM是对java.lang.Object信任的。那自定义的java.lang.Object允许被载入,安全管理器就被绕过了。这样j自定义的类就可以恶意修改任何内容。这显然是不被允许的。
缺点
-
无法让上层加载的“基础”类调用到下层加载的
- 例子:JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK 1.3时加入到rt.jar的),肯定属于Java中很基础的类型了。但JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码
-
无法实现热部署热代码替换
Tomcat的设计是如何破环双亲委派模型
为什么tomcat的设计要打破双亲委派模型,我们先从tomcat设计本身入手。tomcat 作为一个web容器应该具有一下特点:
- 隔离性:web应用类库应该相互隔离,避免依赖库或者应用包互相影响。
- 灵活性:web应用支持热加载,热部署
对于这几个特性,双亲委派模型是显然无法支撑的。对于隔离性而言,双亲委派模型在加载的过程中,并不会有版本的控制,只和你的全限定类名以及类加载器有关系。所以只可能存在一个版本的包,难以支撑。对于灵活性上文也提到双亲委派模型是无法支撑的。
Tomcat的类加载器是如何设计
classDiagram
Bootstrap Class Loader <|-- Extension Class Loader
Extension Class Loader <|-- Application Class Loader
Application Class Loader <|-- CommonClassLoader
CommonClassLoader <|-- CatalinaClass Loader
CommonClassLoader <|-- SharedClass Loader
SharedClass Loader <|-- web App1Loader
SharedClass Loader <|-- web App2Loader
web App1Loader <|-- JasperLoader
可以看到前三个类加载是和默认的一致。CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebappClassLoader则是Tomcat自己定义的类加载器。
- CommonClassLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问
- catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见
- sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见
- WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见(加载/WEB-INF/classes目录下的未压缩的class和资源文件以及/WEB-INF/lib目录下的Jar包)
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
对于web应用类加载起默认的加载顺序:
- 从内存(缓存)中加载
- 如果没有,则从JVM的Bootstrap类加载器加载
- 如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)
- 如果没有,则从父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序为Application、Common、Shared