1. 什么是类加载机制?
代码编译的结果从本地机器码转变为字节码,是存储格式的一小步,确实编程语言发展的一大步。
Java虚拟机把描述类的数据从Class文件加载进内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
虚拟机设计团队把类加载阶段中的”通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这动作的代码模块成为”类加载器”
1.1 类与类加载器的关系
类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每个类的加载器,都拥有一个独立的类命名空间。
这句话通俗的讲:比较两个类是否”相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来自同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
2. 什么是双亲委派模型
- 从Java虚拟机的角度来说,只存在两种不同类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器是使用C++语言实现的(只限定HotSpot),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都是由Java语言实现的,独立于虚拟机外部,并且都继承自抽象类
java.lang.ClassLoader
从Java开发人员的角度来看,类加载还可以划分的更细致一些,比如:
启动类加载器(Bootstrap ClassLoader)
:这个类加载器负责加载存放在JAVA_HOME/lib目录中的或者被-Xbootclasspath
参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录下也不加载)扩展类加载器(Extension ClassLoader)
:这个类加载器由sum.misc.Launcher$ExtClassLoader
实现,它负责加载JAVA_MOME/lib/ext目录下的,或者被java.ext.dirs
系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。应用程序加载器(Application ClassLoader)
:这个类加载器由sun.misc.Launcher$ApplicationLoader
实现。由于这个类加载器是ClassLoader中的getSystemClassLoader
方法的返回值,所以也称为系统类加载器。它负责加载用户路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用中没有定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
这些类加载器之间的关系一般如下图所示:
图中各个类加载器之间的关系成为类加载器的双亲委派模型(Parents Dlegation Mode)
。 双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应由自己的父类加载器加载,这里的类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用父加载器的代码。
类加载器的双亲 委派模型在JDK1.2期间被引入并广泛应用于所有Java程序中,但并不是强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。
2.1 工作流程
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托为父类的加载器去执行,如果父加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式
2.2 为什么要这么做?
如果没有使用双亲委派模型,由各个类加载器自行加载的话,如果用户自己编写一个称为java.lang.Object
类,并放在程序的ClassPath中,那系统将会出现多个不同的Object类,Java类型体系中最基本的行为就无法保证,应用程序会变得混乱。
2.3 双亲委派模型是如何实现的?
所有的代码都在java.lang.ClassLoader
的loadClass
方法之中:
1 | protected Class<?> loadClass(String name, boolean resolve) |
逻辑清晰易懂:先检查是否已经被加载过了,如果没有加载则调用父加载器的loadClass方法,如父加载器为空则默认使用启动类加载器作为父加载器。如果父加载器失败,抛出ClassNotFoundException异常后,再调用自己的findClass方法进行加载
2.4 双亲委派优势
- 采用双亲委派模式的好处是:Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载依次
- 其次时考虑到安全因素,Java核心API中定义类型不会被随意替换,假设通过网络传递一个名为
java.lang.Integer
的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心Java API中发现了这个名字的类,发现该类已经被加载,并不会重新加载网络传递的Integer类,而直接返回已经加载过的Integer.class,这样可以防止核心API库篡改。
如果我们在ClassPath路径下自定义个名为
java.lang.SingleInteger
类呢?该类并不存在java.lang中,经过双亲委派模式,传递到启动类加载器中,由于父加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做时不允许的,因为java.lang是核心API包,需要访问权限,强制加载会报异常1
java.lang.SecurityException: Prohibited package name: java.lang
3. 如何破坏双亲委派模型?
前面提到,双亲委派模型不是一个强制性的约束模型,而是一个建议型的类加载器实现方式。在Java中大部份的类加载器都遵循这模型,但是也会存在特殊情况导致被双亲委派模型破坏。
通过预加载的方式
通过线程上下文类加载器Thread.getContextClassLoader();这个类加载器可以通过java.lang.Thread类的setContextClassLoader方法进行设置。如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过多的话,那这个类加载器默认即使应用程序类加载器
3.1 预加载
这里通过一个简单的例子,就拿sql连接来说:
java.sql.DriverManager
: rt.jar包中的类,通过Boostra类加载器加载。
- DriverTest:开发人员自定义的实现
java.sql.Driver
接口的类,通过Application 加载器加载
开发人员通过DriverManager.registerDriver方法把自己实现的获取连接的Driver实现类加载并注册到DriverManager中。然后DriverManager.getConnection方法会遍历所有注册的Driver,并触发Driver的connect接口来获取连接。(即绕过在DriverManager所在的Bootstrap加载器,因为Bootstrap加载器不能加载开发人员实现的Driver类)
1 | public class DriverTest implements Driver { |
运行main方法打印输出:
1 | who load DriverTest:sun.misc.Launcher$AppClassLoader@18b4aac2 |
DriverManager是由Boostrap加载器加载的,因而获取不了Boostrap加载器,所以为null,从双亲委派模型的机制上看,因为rt.jar是由Boostrap加载器加载的,所以里面的类,都不能用到rt.jar以外的类。
那么DriverManager.getConnection是怎么调用DriverTest(App加载器)的getConnection方法呢?
因为双亲委派模型的限制,DriverManager不肯呢个自己去加载DriverTest,DriverTest的加载实际上是由AppClassLoader完成的,DriverTest里面会往DriverManager中注册一个驱动
1 | java.sql.DriverManager.registerDriver(new DriverTest()); |
对于DriverManager而言,它不关注driver的加载,只需要遍历所有注册的Driver,然后检查驱动类是否能被”调用类的类加载器””识别,如果可以识别,则调用driver.connect方法
1 | public class DriverManager{ |
整体的流程:
3.2 Thread.getContextClassLoader()
通过调用Thread.getContextClassLoader()
相当于直接拿类加载器,自然不用通过向上递归走双亲委派类。
举个例子:rt.jar中的javax.xml.parsers.FactoryFinder
中的newInstance方法:
在newInstance中会用到
getProviderClass
方法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/**
* Create an instance of a class. Delegates to method
* <code>getProviderClass()</code> in order to load the class.
*
* 定义的JNDI接口
* @param type Base class / Service interface of the factory to instantiate
* JNDI的实现类名
* @param className Name of the concrete class corresponding to the service provider
* 加载器:如果为null,则通过线程的上下文加载器进行加载
* @param cl <code>ClassLoader</code> used to load the factory class. If <code>null</code>
* current <code>Thread</code>'s context classLoader is used to load the factory class.
* 如果为true,则使用bootstrap加载器。
* @param useBSClsLoader True if cl=null actually meant bootstrap classLoader. This parameter
* is needed since DocumentBuilderFactory/SAXParserFactory defined null as context classLoader.
*/
static <T> T newInstance(Class<T> type, String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader)
throws FactoryConfigurationError
{
//省略一堆代码
try {
//在这个方法里面,可以通过线程上下文加载器进行加载className对应的类
Class<?> providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
//省略一堆代码
}
}在
getProviderClass
中会用到SecuritySupport.getContextClassLoader
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22static private Class<?> getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, false, FactoryFinder.class.getClassLoader());
} else {
//在这里,会获得线程的上下文加载器去加载类
//其中 ss是 SecuritySupport.java
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
return Class.forName(className, false, cl);
}
}
}
//省略一堆代码
}
在SecuritySupport.getContextClassLoader中会用到Thread.currentThread().getContextClassLoader()拿到线程上下文类加载器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class SecuritySupport {
ClassLoader getContextClassLoader() throws SecurityException{
return (ClassLoader)
AccessController.doPrivileged(new PrivilegedAction() {
public Object run() {
ClassLoader cl = null;
//try {
//获得线程的上下文加载器
cl = Thread.currentThread().getContextClassLoader();
//} catch (SecurityException ex) { }
if (cl == null)
cl = ClassLoader.getSystemClassLoader();
return cl;
}
});
}
4. Tomcat的类加载器是怎么设计的?
问题一:Tomcat如果使用默认的类加载机制行不行?
可以思考一下:Tomcat是个web容器,那么它要解决什么问题?
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,相互隔离
- 部署在同一个web容器中相同的类库相同的版本可以共享。
- web容器也有自己依赖的类库,不能和应用程序的类库混淆,基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们直到,jsp文件最终也要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,所以,web容器需要支持jsp修改后不用重启
如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
问题二:Tomcat如果使用默认的类加载机制行不行?
答案是不行的。默认的类加载器是能够实现的,因为它的职责就是保证唯一性。我们想我们要怎么实现jsp文件的热修改,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。
那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件
5. Tomcat 如何实现自己的类加载机制
从图中的委派关系可以看出:
CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用
CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
- WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
- JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。
前面3个类加载器和默认的一致,CommonClassLoader
、CatalinaClassLoader
、SharedClassLoader
和WebappClassLoader
则是Tomcat自己定义的类加载器。它们分别加载/common/*
、/server/*
、/shared/*
(在tomcat6之后合并到根路径下的lib目录下)、和/WebApp/WEB-INF/*
中的Java类库。其中WebApp类加载器和JSP类加载器通常会存在多个实例,每一个Web应用程序对应一个WebApp类加载器,每一个JSP文件对应一个JSP类加载器
commonLoader
:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容器本身以及各个Webapp访问;
catalinaLoader
:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不可见;
sharedLoader
:各个Webapp共享的类加载器,加载路径中的class对于所有Webapp可见,但是对于Tomcat容器不可见;
WebappClassLoader
:各个Webapp私有的类加载器,加载路径中的class只对当前Webapp可见;
5.1 Tomcat 违背了双亲委派模型吗?
Tomcat 违背了双亲委派模型。因为双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当由自己的父类加载器加载。
而 Tomcat
不是这样实现,Tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器。