Hexo

点滴积累 豁达处之

0%

02 tomcat类加载

在JVM中并不是一次性把所有的文件都加载到,而是一步一步的,按照需要来加载。

java类加载模式与web容器的类加载模式

1 概述

​ 任何一个java文件都是先编译成 .class文件,然后再经过jvm解释成机器码。只要拥有.class文件和jvm,那么任何一个平台都可以运行

2 ClassLoader 简介

2.1 ClassLoader 类几个重要方法:

  • getParent() 返回该类加载器的父类加载器。
  • loadClass(String name) 加载名称为name的类,返回的结果是 java.lang.Class类的实例。
  • findClass(String name) 查找名称为name的类,返回的结果是 java.lang.Class类的实例。
  • findLoadedClass(String name) 查找名称为name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
  • defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
  • resolveClass(Class c) 链接指定的 Java 类。

2.2 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
protected synchronized Class loadClass(String name, boolean resolve) 
throws ClassNotFoundException{
// 首先检查该name指定的class是否有被加载
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//如果parent不为null,则调用parent的loadClass进行加载
c = parent.loadClass(name, false);
}else{
//parent为null,则调用BootstrapClassLoader进行加载
c = findBootstrapClass0(name);
}
}catch(ClassNotFoundException e) {
//如果仍然无法加载成功,则调用自身的findClass进行加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

2.3 ClassLoader抽象类中默认实现的两个构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected ClassLoader() {  
        SecurityManager security = System.getSecurityManager();  
       if (security !=null) {  
            security.checkCreateClassLoader();  
        }  
       //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器  
       this.parent = getSystemClassLoader();  
        initialized =true;  
    }  
   protected ClassLoader(ClassLoader parent) {  
        SecurityManager security = System.getSecurityManager();  
       if (security !=null) {  
            security.checkCreateClassLoader();  
        }  
       //强制设置父类加载器  
       this.parent = parent;  
        initialized =true;  
    } 

3 java的类加载流程和双亲委派模型

3.1 类加载器介绍

  1. 从Java虚拟机的角度来说,只存在两种不同类加载器:一种是**启动类加载器(Bootstrap ClassLoader)**,这个类加载器使用C++语言实现(只限HotSpot),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader
  2. 从Java开发人员的角度来看,类加载还可以划分的更细致一些,绝大部分Java程序员都会使用以下3种系统提供的类加载器:
    • 启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 /lib 下面的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。
    • 标准扩展(Extension)类加载器:它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。
    • 系统(System)类加载器:它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中。开发者可以直接使用系统类加载器。

3.2 类加载器的创建

这三个加载器都是jvm通过一个Launcher类来初始化创造的 ,三个类加载器的创建分别在这相应的三个内部类中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*静态内部类继承了URLClassLoader*/
static class AppClassLoader extends URLClassLoader {
final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
/*得到classpath下的所有类的路径*/
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null?new File[0]:Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null?new URL[0]:Launcher.pathToURLs(var2);
/*这里创建AppClassLoader,注意这里并没有去加载路径下的类*/
return new Launcher.AppClassLoader(var1x, var0);
}
});
}

ExtentionClassLoader 类似于这种方式去创建,那BootStrapClassLoader是怎么创建的呢? 其实java中并没有这个类,只是一个概念,它嵌入到jvm中了,由c++编写的。也就是jvm一启动就会有这个类,但是在Launcher类中规定了它加载类所在的路径

1
private static String bootClassPath = System.getProperty("sun.boot.class.path");

从上面的代码也看到了 AppclassLoader 继承的是URLClassLoader,但它的父加载器却是ExtentionClassLoader ,采用这种方式是有两个方面:

  • 继承URLClassLoader,是为了继承这个类中的findClass()方法为了找到规定路径下的类。继续看URLClassLoader的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
。。。。
public Class<?> run() throws ClassNotFoundException {
/*解析类的路径*/
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
/*通过字节码文件的路径获得相应的Class类*/
return defineClass(name, res);
}
。。。。。
return result;
}

前面有个问题是只是找到字节码文件,并没有加载它,答案就在这里,通过defineClass(name, res)来加载它

**为什么不在loadClass()中加载它,而是通过findClass()来加载? **

  • 所有类加载器中的基类ClassLoader中有一个属性
1
private final ClassLoader parent;

是通过类与类组合的方式来定义一个父加载器的,为了构成双亲委派模型。

3.3 双亲委派模型

双亲委托模型

双亲委派机制的工作流程

  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

3.4 三个类加载器的关系

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {  
   try {  
     System.out.println(ClassLoader.getSystemClassLoader());  
     System.out.println(ClassLoader.getSystemClassLoader().getParent();  
     System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());  
   } catch (Exception e) {  
       e.printStackTrace();  
   }  

1
2
3
4
代码输出如下:  
sun.misc.Launcher$AppClassLoader@197d257  
sun.misc.Launcher$ExtClassLoader@7259da  
null  

​ 通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。我们还是借助于代码分析一下:

4 类加载疑问

4.1 那么为什么要设计双亲委派模型呢?

假设,整个系统中没有这个设计,还是上面的程序,没有双亲委派模型,那么String这个类就由AppclassLoader加载了,然后可能在有另一个自定义的加载器加载String这个类了,那么程序就乱套了!!到处都是真假难辨的String。而有了双亲委派模型,不管是哪个加载器加载的String,这个类都是由BootStrapClassLoader加载。双亲委派模型最主要的作用是保证java核心类的安全性。

注意:一个类可以被多次加载,但是一个加载器只能加载一次,而且判断一个类是不是相同的,是比较 包名+类名+类加载器id  当某个classloader加载的所有类实例化的对象都被GC回收了,那么这个加载器就会被回收掉。

4.2 双亲委派模型是不可改变的吗?

首先,明确双亲委派模型是一种规范,在自定义类加载器的时候完全可以重写loadClass()方法中的逻辑。这里回答一下前面的问题,为什么不用loadClass()实现类加载的功能,而是用findClass()。这是为了把委派模型的逻辑和类加载器要实现的逻辑分离开了。所以一般自定义类加载器loadClass()一般不动,而是重写findClass()。

4.2.1 什么时候破坏双亲委托

4.2.1.1 java需要连接数据库

来看一个例子java需要连接数据库,但是数据库的品种这么多,每家厂商写的程序都不同,为了让每个厂商统一,java制定了一系列规范的接口,放在BootStrapClassLoader可以加载的路径中。所有厂商只要按照这些接口规范写就好了,并且统一有一个管理的类在DriverManager。这样java制定者和厂商都开开心心,这时候出现了一个问题。 根据委派模型,A类中引用了B类

1
2
3
public class A{
private B = new B();
}

那么默认的B的加载器也是由A的加载器加载的!!

所以通过DriverManager统一来得到Driver的话,那么BootStrapClassLoader默认是加载 java.sql 包下的Driver接口。但实际上必须要加载它的实现类。 可是,根据委派模型,父类加载器去调用子类加载器是不可能完成的 必须由BootStrapClassLoader来加载,这就难办了,BootStrapClassLoader说我的工作就是加载 %JRE_HOME%\lib 下的,要我加工作量,不干!!况且我也找不到在哪啊,我只知道它的接口啊,并不知道它的实现类

于是,出现了一个设计,在线程Tread类中内置一个

1
2
/* The context ClassLoader for this thread 默认是appclassloader ,可以自己设置*/
private ClassLoader contextClassLoader;

因此在父类加载器需要调用子类加载器的时候,就可以通过

1
Thread.currentThread().getContextClassLoader();

来获取想要的类加载器。  下面来看JDBC的源码:  在java.sql.DriverManager中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//从Thread.currentThread中取出appclassloader
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

是怎么使用这个classLoader的呢?

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
//是通过Class.forName()并指定类加载器。
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
}
。。。。
return result;
}
4.2.1.2 web中的热部署

java web中的热部署,指的是热部署是在不重启 Java 虚拟机的前提下,能自动侦测到 class 文件的变化,更新运行时 class 的行为。在编写java程序的时候,每一次改动源代码,就必须重启一次应用程序,那java的这个缺陷是由什么导致的呢?

  1. java类只能由类加载器加载一次,这样在改动代码后,如果不重启就不能被重新加载。
  2. java类一般被系统自带的appclassloader 来加载。

然而热部署对于开发来说实在太重要了,想想当你调试js代码的时候,一个屏幕源代码,一个屏幕浏览器,随时随地的观察代码带来的变化。而java对于这一点,由于它语言的特性导致很难做到这一点。  如果想要实现热部署,需要做哪些工作呢?

  • 销毁ClassLoader
  • 创建新的ClassLoader去加载更新后的class类文件。

第一步销毁ClassLoader,如果要做到这一步,那么这个类文件一定不能是appclassloader加载的,因此要自定义ClassLoader。 第二步,要做到的话,首先必须有一个监听器去监听类发生变化,然后才能相应的创建ClassLoader去加载。 这也是Tomcat服务器在处理类加载的时候进行的做法,每一个web应用都有一个相应的类加载器。原理图如下:

WEB应用原理

并且Tomcat自定义的类加载器并不遵循双亲委派模型,而是先检查自身可不可以加载这个类,而不是先委派。前面也提到了,BootStrap,System和Common 这三个类加载器遵守委派模型同时加载java以及tomcat本身的核心类库。这样做的好处是更好的实现了应用的隔离,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。(当然可以配置使得不是所有的app都要被加载,默认是全部加载) Tomcat中当类发生改变时,监听器监听到触发StandardContext.reload(),然后销毁以前的类加载器,重新创造一个类加载器。

**小结: **

  1. Bootstrap类加载器是用本地代码实现的类装入器,不允许不允许直接通过引用进行操作。
  2. Java提供的类加载器都会继承ClassLoader抽象类 。
  3. 系统类加载器(AppClassLoader)为默认的父加载器。
  4. 父子加载器并非继承关系,系统类加载器的父加载器是标准扩展类加载器,标准扩展类加载器本身强制设定父类加载器为null。
  5. 当委托父类加载器去加载,如果父加载器为null,会调用Bootstrap类加载器加载。
  6. 开发者创建类加载器是通过调用createClassLoader方法实现的 。
  7. CommonClassLoader能加载的类都可以被Catalina ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离
  8. 各个WebAppClassLoader实例之间相互隔离。
  9. JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。