JVM学习笔记03-打破双亲委派机制(Tomcat) Tom

Tomcat为何要打破双亲委派机制

开始之前,我们有个问题可以探讨一下:Tomcat使用默认的双亲委派类加载机制是否可行?

首先,我们可以思考一下,Tomcat作为一个web容器,它需要解决什么问题?

  1. 假如有若干个应用程序部署在Tomcat上,这些应用程序可能会依赖到同一第三方类库的不同版本,因此Tomcat必须支持每个应用程序的类库可以相互隔离
  2. 部署在同一个Tomcat上的不同应用程序,相同类库的相同版本应该是共享的,否则就会出现大量相同的类加载到虚拟机中
  3. Tomcat本身也有依赖的类库,与应用程序依赖的类库可能会混淆,基于安全考虑,应该将两者进行隔离
  4. 要支持Jsp文件修改后,其生成的class能在不重启的情况下及时被加载进JVM

那么,采用默认的双亲委派类加载机制,能否解决上述问题呢?

  • 问题1、3,如果Tomcat采用默认的双亲委派加载机制,是无法加载同一类库不同版本的类的,因为默认的双亲委派加载机制在加载类时,是通过类的全限定名做唯一性校验的
  • 问题2,默认的双亲委派类加载机制可以实现,因为它本就能保证唯一性
  • 问题4,我们知道Jsp文件更新其实也就是class文件更新了,此时类的全限定名并没有改变,修改Jsp文件后,类加载器会从方法区中直接取到已存在的,这会导致修改后Jsp文件其实不会重新加载。那么,如果直接卸载掉这个Jsp文件的类加载器,再重新创建类加载器去加载修改后的Jsp文件,不就能解决问题了吗?那么你应该能猜到每个Jsp文件应对应一个唯一的类加载器

到此,我们可以得出答案,Tomcat只使用默认的双亲委派类加载机制是不可行的

Tomcat中的自定义类加载器

Tomcat的自定义类加载器 (1).png

Tomcat的几个主要的自定义类加载器

  • CommonClassLoader:公共的类加载器,其加载的class可以被Tomcat容器本身以及各个Webapp访问
  • CatalinaClassLoader:私有的类加载器,其加载的class对于Webapp不可见
  • ShareClassLoader:各个Webapp共享的类加载器,其加载的class对于所有Webapp可见,但对于Tomcat容器本身不可见
  • WebappClassLoader:各个Webapp私有的类加载器,其加载的class只对当前的Webapp可见

从上面的图,不难看出:

  • CommonClassLoader能加载的类都可以被CatalinaClassLoaderShareClassLoader使用,从而实现公有类库的公用,而CatalinaClassLoaderShareClassLoader各自加载的类则与对方相互隔离
  • WebappClassLoader可以使用ShareClassLoader加载的类,但各个WebappClassLoader之间相互隔离
  • JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那个.class文件,一对一的设计是为了随时丢弃它,当Tomcat检测到JSP文件被修改时,会替换掉当前的JasperLoader的实例,并通过再一次建立一个新的JasperLoader实例来实现JSP文件的热加载功能

由此可知,Tomcat的设计是违背Java的双亲委派模型的,每个WebappClassLoader加载自己目录下的.class文件,不会传递给父加载器,这就打破了双亲委派机制,这样做正是为了实现隔离性。

下面这段代码模拟实现了Tomcat的WebappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码package org.laugen.jvm;
import sun.misc.PerfCounter;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class TestCustomizeClassLoader {
static class CustomizeClassLoader extends ClassLoader {
private String classPath;

public CustomizeClassLoader(String classPath) {
this.classPath = classPath;
}

// 读取class字节码文件
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
long t1 = System.nanoTime();
if (name.startsWith("org.laugen.jvm.Note")) {
c = findClass(name);
} else {
c = this.getParent().loadClass(name);
}
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

}

public static void main(String args[]) throws Exception {
CustomizeClassLoader classLoader1 = new CustomizeClassLoader("D:/MyClasses-v1");
System.out.println("自定义类加载器的父加载器:" + classLoader1.getParent().getClass().getName());
Class clazz1 = classLoader1.loadClass("org.laugen.jvm.Note");
Object obj1 = clazz1.newInstance();
Method method1 = clazz1.getDeclaredMethod("print", null);
method1.invoke(obj1, null);
System.out.println("Note类的类加载器是:" + clazz1.getClassLoader().getClass().getName());

System.out.println("====================================================================");

CustomizeClassLoader classLoader2 = new CustomizeClassLoader("D:/MyClasses-v2");
System.out.println("自定义类加载器的父加载器:" + classLoader2.getParent().getClass().getName());
Class clazz2 = classLoader2.loadClass("org.laugen.jvm.Note");
Object obj2 = clazz2.newInstance();
Method method2 = clazz2.getDeclaredMethod("print", null);
method2.invoke(obj2, null);
System.out.println("Note类的类加载器是:" + clazz2.getClassLoader().getClass().getName());
}
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类(V1版本)
创建了org.laugen.jvm.Note类的实例(V1版本)
这是一个note(V1版本)
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader
====================================================================
自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类(V2版本)
创建了org.laugen.jvm.Note类的实例(V2版本)
这是一个note(V2版本)
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader

由此可知,在同一个JVM中,两个相同全限定名的类对象可以共存,因为他们的类加载器可以不一样,所以看两个类对象是否是同一个时,出了要看类的全限定名外,还需要看他们的类加载器是不是同一个

那么,你知道Tomcat的JasperLoader热加载是怎么实现的吗?

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%