JVM

【深入理解Java虚拟机】【09】类加载及执行子系统的案例与实战

Posted by Charlie on 2019-09-10

[TOC]

Tomcat:正统的类加载器架构

主流的Java Web服务器,如Tomcat、Jetty、WebLogic、WebSphere或其他笔者没有列举 的服务器,都实现了自己定义的类加载器(一般都不止一个)。因为一个功能健全的Web服 务器,要解决如下几个问题:

  1. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以实现相互隔离。

    这是 最基本的需求,两个不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求一 个类库在一个服务器中只有一份,服务器应当保证两个应用程序的类库可以互相独立使用。

  2. 部署在同一个服务器上的两个Web应用程序所使用的Java类库可以互相共享。

    这个需求 也很常见,例如,用户可能有10个使用Spring组织的应用程序部署在同一台服务器上,如果 把10份Spring分别存放在各个应用程序的隔离目录中,将会是很大的资源浪费——这主要倒 不是浪费磁盘空间的问题,而是指类库在使用时都要被加载到服务器内存,如果类库不能共 享,虚拟机的方法区就会很容易出现过度膨胀的风险。

  3. 服务器需要尽可能地保证自身的安全不受部署的Web应用程序影响。

    目前,有许多主流 的Java Web服务器自身也是使用Java语言来实现的。因此,服务器本身也有类库依赖的问 题,一般来说,基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。

  4. 支持JSP应用的Web服务器,大多数都需要支持HotSwap功能。

    JSP文件最终 要编译成Java Class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的 概率远远大于第三方类库或程序自身的Class文件。而且ASP、PHP和JSP这些网页应用也把 修改后无须重启作为一个很大的“优势”来看待,因此“主流”的Web服务器都会支持JSP生成类 的热替换,当然也有“非主流”的,如运行在生产模式(Production Mode)下的WebLogic服务 器默认就不会处理JSP文件的变化。

由于存在上述问题,在部署Web应用时,单独的一个ClassPath就无法满足需求了,所以 各种Web服务器都“不约而同”地提供了好几个ClassPath路径供用户存放第三方类库,这些路 径一般都以“lib”或“classes”命名。被放置到不同路径中的类库,具备不同的访问范围和服务 对象,通常,每一个目录都会有一个相应的自定义类加载器去加载放置在里面的Java类库。

在Tomcat目录结构中,有3组目录(“/common/”、“/server/”和“/shared/”)可以存放 Java类库,另外还可以加上Web应用程序自身的目录“/WEB-INF/”,一共4组

tomcat_classloader

  1. /common:类库可以被Tomcat和所有的Web应用使用。->CommonClassLoader
  2. /server:类库可以被Tomcat使用,对所有Web应用程序都不可见。->CatalinaClassLoader
  3. /shared:类库对所有Web应用程序可见,但是对Tomcat自己不可见。->SharedClassLoader
  4. /WebApp/WEB-INF:仅对此Web应用程序可见,对其他Web应用程序和Tomcat都不可见。->WebAppClassLoader

OSGi:灵活的类加载器架构

字节码生成技术与动态代理的实现

代码示例:

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DynamicProxyTest {

interface IHello {
void sayHello();
}

static class Hello implements IHello {
@Override
public void sayHello() {
System.out.println("hello world");
}
}

static class DynamicProxy implements InvocationHandler {
Object originalObj;

Object bind(Object originalObj) {
this.originalObj = originalObj;
return Proxy.newProxyInstance(
originalObj.getClass().getClassLoader(),
originalObj.getClass().getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println("welcome");
return method.invoke(originalObj, args);
}

}

public static void main(String[] args) {
IHello ihello = (IHello) new DynamicProxy().bind(new Hello());
ihello.sayHello();
}

}

Proxy.newProxyInstance()方法返回一个实现了IHello的接口,并且代理了new Hello()实例行为的 对象。最后它调用了 sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动作,这个方法可以 在运行时产生一个描述代理类的字节码byte[]数组。

如果想看一看这个在运行时产生的代理 类中写了些什么,可以在main()方法中加入

1
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles""true");

磁盘中将会产生一个名为“$Proxy0.class”的代理类Class 文件,反编译后可以看见

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
package org.fenixsoft.bytecode;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
public final class $Proxy0 extends Proxy
implements DynamicProxyTest.IHello
{
private static Method m3;
private static Method m1;
private static Method m0;
private static Method m2;
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
}
public final void sayHello()
throws
{
try
{
this.h.invoke(this,m3,null);
return
}
catch(RuntimeException localRuntimeException)
{
throw localRuntimeException;
}
catch(Throwable localThrowable)
{
throw new UndeclaredThrowableException(localThrowable);
}
}
//此处由于版面原因,省略equals()、hashCode()、toString()三个方法的代码
//这3个方法的内容与sayHello()非常相似。
static
{
try
{
m3=Class.forName("org.fenixsoft.bytecode.DynamicProxyTest $IHello").getMethod("sayHello"new Class[0]);
m1=Class.forName("java.lang.Object").getMethod("equals"new Class[]{Class.forName("java.lang.Object")});
m0=Class.forName("java.lang.Object").getMethod("hashCode"new Class[0]);
m2=Class.forName("java.lang.Object").getMethod("toString"new Class[0]);
return
}
catch(NoSuchMethodException localNoSuchMethodException)
{
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch(ClassNotFoundException localClassNotFoundException)
{
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}

这个代理类的实现代码也很简单,它为传入接口中的每一个方法,以及从 java.lang.Object中继承来的equals()、hashCode()、toString()方法都生成了对应的实 现,并且统一调用了InvocationHandler对象的invoke()方法(代码中的“this.h”就是父类 Proxy中保存的InvocationHandler实例变量)来实现这些方法的内容,各个方法的区别不过是 传入的参数和Method对象有所不同而已,所以无论调用动态代理的哪一个方法,实际上都是 在执行InvocationHandler.invoke()中的代理逻辑。