素小暖讲JVM:第九章 类加载与执行子系统的案例与实战

假装没事ソ 提交于 2020-02-25 19:20:24

本系列是用来记录《深入理解Java虚拟机》这本书的读书笔记。方便自己查看,也方便大家查阅。

欲速则不达,欲达则欲速!

第九章 类加载与执行子系统的案例与实战

代码编译的结果从本地机器码转变为字节码是存储格式发展的一小步,却是编程语言发展的一大步。

一、案例分析

1、tomcat:正统的类加载器架构

主流的java web服务器,如tomcat、jetty、weblogic、websphere等服务器,都实现了自定义的类加载。因为一个功能健全的web服务器,要解决以下问题:

(1)部署在同一个服务器上的两个web应用程序所使用的java类库可以实现相互隔离。服务器应当保证两个应用程序的类库可以互相独立使用。

(2)部署在同一个服务器上的两个web应用程序所使用的java类库可以互相分享。如果部分类库不能分享,虚拟机的方法区就会很容易出现过度膨胀的风险。

(3)服务器需要尽可能的保证自身的安全不受部署的web应用程序影响。基于安全考虑,服务器所使用的类库应该与应用程序的类库互相独立。

(4)支持JSP应用的web服务器,大多数需要支持hotswap功能。我们知道,JSP文件最终要编译成java class才能由虚拟机执行,但JSP文件由于其纯文本存储的特性,运行时修改的概率远远大于第三方类库或程序自身class文件。

由于存在上述问题,在部署web应用时,单独的一个classpath就无法满足需求了,所以各种web服务器都不约而同的提供了好几个classpath路径供影虎存放第三方类库,这些路径一般以lib或classes命名。不同路径的类库,具备不同的访问范围和服务对象。

在tomcat目录结构中,有3种目录( “/common/*”、“/server/*”和“/shared/*” )可以存放java类库,另外加上web应用程序自身目录 “WEB-INF/*”,一共4组,把java类库放置在这些目录中的含义分别是:

  • 放置在/common目录中:类库可被tomcat和所有的web应用程序共同使用。
  • 放置在/server目录中:类库可被tomcat使用,对所有web应用程序不可见。
  • 放置在/shared目录中:类库可被所有的web应用程序共同使用,但对tomcat不可见。
  • 放置在 /WebApp/WEB-INF目录中: 类库仅仅可以被此web应用程序使用,对tomcat和其它web应用程序都不可见

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,tomcat自定义多个类加载器,这些类加载器按照经典的双亲委派模型来实现。

从图中的委派关系可以看出, CommonClassLoader 能加载的类都可以被 CatelinaClassLoader和SharedClassLoader使用 , 而CatelinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

而JasperLoader的加载范围则仅仅是这个JSP文件所编译出来的哪一个Class,它出现的目的就是为了被抛弃:当服务器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

注意:对于Tomcat6.x的版本,只有指定了 tomcat/conf/catalina.properties 配置文件的 server.loader 和 share.loader 项才会真正建立对应的 *ClassLoader实例,否则会用到这两个类加载器的地方使用CommonClassLoader 的实例来代替,默认配置中没有设置这两个loader 项。

2、OSGi:灵活的类加载器架构

(1)OSGi概述

OSGi里,Bundle之间的依赖关系从传统的上层模块依赖底层模块转变为平级模块之间的依赖。

OSGi特点,要归功于它灵活的类加载架构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。

java程序社区流传着这么一个观点:“学习J2EE规范,去看JBoss源码;学习类加载器,就去看OSGi源码”。尽管J2EE规范和类加载器的知识并不是一个对等的概念。不过既然这个观点能在程序员中流传开来,也从侧面说明了OSGi对类加载器的运用确实有其独到之处。

OSGi(open service gateway initiative)是OSGI联盟制定的一个基于java语言的动态模块化规范,最著名的应用案例就是eclipse IDE。

(2)引入OSGi的一个重要理由:

  • 更精确的模块划分和可见性控制
  • 基于OSGi的程序很可能可以实现模块级的热插拔功能,当程序升级更新或调试出错时,可以只停用,重新安装后启用程序的其中一部分,这对企业级程序来说是一个非常有诱惑力的特性。

OSGi之所以能有上述诱人的特点,要归功于它灵活的类加载器结构。OSGi的Bundle类加载器之间只有规则,没有固定的委派关系。例如,某个Bundel声明了一个它依赖的Package,如果有其他Bundle声明发布了这个Package后,那个对这个Package的所有类加载动作都会给发布它的Bundle类加载器去完成。不涉及某个具体的Package时,各个Bundle加载器都是平级的关系,只有具体使用到某个Package和Class的时候,才会根据Package导入导出定义来构造Bundle间的委派。

另外,一个Bundle类加载器为其它Bundle提供服务时,会根据 Export-Package 列表严格控制访问范围。如果一个类存在于Bundle的类加载器能找到这个类,但不会提供给其它bundle使用,而且OSGi平台也不会把其它Bundle的类加载请求分配给这个Bundle来处理。

(3)代码实例

我们可以举一个更具体一点的简单例子,加入存在BundleA,BundleB和BundleC三个模块,并且这三个Bundle定义的依赖关系为:

  • Bundle A:声明发布了packageA,依赖了java.*包;
  • Bundel B:声明依赖了PackageA和PackageC,同时也依赖了java.*包;
  • Bundle C:声明发布了packageC,依赖了PackageA.
那么,这三个Bundle之间的类加载器及父类 加载器之间的关系如图9-2所示。
 
由于没有牵扯到哦具体的OSGi实现,图中的类加载器都没有指明具体的加载器实现,只是一个体现了加载器间关系的概念模型,并且只是体现了OSGi中最简单的加载器委派关系。一般来说,在OSGi里加载一个雷可能发生的查找行为和委派关系会比图9-2中显示的复杂的多,类加载时可能进行的查找规则如下:
  • 以java.*开头的类,委派给父类加载器加载。
  • 否则,委派列表名单内的类,委派给父类加载器加载。
  • 否则,Import列表中的类,委派给Export这个类的Bundle的类加载器加载
  • 否则,查找当前Bundle的Classpath,使用自己的类加载器加载。
  • 否则,查找是否在自己的Fragment Bundle中,如果是则委派给Fragment Bundle的类加载器加载。
  • 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle类加载器加载。
  • 否则,查找失败。
从图9-2中还可以看出,在OSGi里面,加载器之间的关系不再是双亲委派模型的树形结构,而是已经进一步发展成了一种运行时才能确定的网状结构。这个网状结构的类加载器架构在带来更优秀的灵活性的同时,也可能会产生许多新的隐患。笔者曾经参与过讲一个非OSGi的大型系统向Equinox OSG平台迁移的项目,由于历史原因,代码模块之间的依赖关系错综复杂,勉强分离出各个模块的Bundle后,发现在 高并发环境下经常出现死锁。我们很容易就找到了死锁的原因:如果出现了Bundle A依赖Bundle B的Package B,而Bundle B又依赖了Bundel A的Package A,这两个Bundle进行类加载时就很容易发生死锁。具体情况是当Bundel A 加载Package B时,首先需要锁定但钱磊加载器的实例对象(java.lang.ClassLoader.loadClass()是一个synchronized方法),然后把请求委派给Bundle B的加载器处理,但如果这时候Bundle B的也正好想加载Package A的类,它也先锁定自己的加载器再去请求Bundle A的加载器处理,这样两个加载器都在等待对方处理自己的请求,而对方处理完之前自己又一直处于同步锁定的状态,因此他们就互相死锁,永远无法完成加载请求了。Equinox的Bug List中也有关于这类问题的Bug,并提供了一个以牺牲戏能为代价的解决方法-用户可以启用osgi.classloader.singleThreadLoads参数来按单线程串行化的方式强制进行类加载动作。在JDK1.7中,将会为非树状继承关系下的类加载器架构进行一次专门的升级,希望从底层避免这类死锁的状况,这个动作也是为将实现“官方”模块化规范-JSR-296,JSR-277座准备。
总体来说,OSGi描绘了一个很美好的模块化开发的目标,而且定义了实现这个目标所需要的各种服务,同时也有成熟框架对其提供实现支持。对于单个虚拟机下的应用,从开发初期就建立在OSGi上是一个很不错的选择,这样便于约束依赖。但并非所有的应用都适合采用OSGi作为架构基础,OSGi在提供强大功能的同时也引入的额外的复杂度,带来了线程死锁和内存泄漏的风险。
 
(4)字节码生成技术与动态代理的实现
 
“字节码生成”并不是什么高深的技术读者在看到“字节码生成”这个标题时也不必先去想诸如Javassist,CGLib和ASM之类的字节码类库,因为JDK里面的javac命令就是字节码生成技术的“老祖宗”,并且javac也是一个由java语言写成的程序,它的代码存放在OpenJDK的jdk7/langtools/src/share/classes/com/sun/tools/javac目录中。要深入了解字节码生成,阅读源码是一个很好的途径,不过javac对于我们这个例子来说太庞大了。在java里面除了javac和字节码类库外,使用到字节码生成的例子还有很多,如Web服务器中的JSP编译器,编译时植入的AOP框架,还有很常用的动态代理技术,甚至在使用反射的时候虚拟机都有可能会在运行时生成字节码来提高执行速度。我们选择其中相对简单的动态代理来看看字节码生成技术是如何影响程序运作的。
相信许多Java开发人员都使用过动态代理,即使没有直接使用过java.lang.reflect.Proxy或实现过java.lang.reflect.invocationHandler接口,应该也用过Spring来做过Bean的组织管理。如果使用过Spring,那大多数情况下就都用过动态代理,因为如果Bean是面向接口编程,那么在Spring内部则都是铜鼓动态代理的方式来对Bean进行增强的。动态代理中所谓的“动态”,是针对使用Java代码实际编写了代理类的“静态”代言而言的,它的优势不在于省去了编写代理类那一点工作量,而是实现了可以在原始类和接口还未知的时候,就确定代理类的代理行为,当代理类与原始类脱离直接关系后,接可以很灵活地重用于不同的应用场景之中。
代码清单9-1中演示了一个最简单的动态代理的用法,原始的逻辑是打印一句“hello world”,代理类的逻辑实在原始类的方法执行前打印一句“welcome”。我们先看一下代码,然后再分析JDK是如何做到的。

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 hello = (IHello)new DynamicProxy().bind(new Hello());
		hello.sayHello();
	}
}

上述代码中,唯一的“黑匣子”就是Proxy.newProxyInstance()方法,除此之外再没有任何特殊之处。这个方法返回一个实现了IHello的接口。并且代理了new Hello()实例行为的对象。跟踪这个方法的源码,可以看到程序进行了验证,优化,缓存,同步,生成字节码和显式类加载等操作,前面的步骤并不是我们关注的重点,而最后它调用了sun.misc.ProxyGenerator.generateProxyClass()方法来完成生成字节码的动作.。

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

相信许多Java开发人员都是用过动态代理,例如 java.lang.reflect.Proxy 或实现过 java.lang.reflect.InvocationHandler 接口。

下面一个例子,在方法前面打印一句“welcome”。

package jvm;

public interface IHello {
    void sayHello();
    void sayHi();
}
package jvm;

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

    @Override
    public void sayHi() {
        System.out.println("hi world");
    }
}
package jvm;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public 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);
    }
}
package jvm;

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

运行结果

4、Retrotranslator:跨越JDK版本

把JDK1.5 中编写的代码放到 JDK1.4 或 1.3 的环境去部署使用。为了解决这个问题,一种名为“Java逆转移植”的工具(Java Backporting Tools)应运而生,Retrotranslator 是这类工具中较为出色的一个。

二、实战:自己动手实现远程执行功能

我们将使用前面学到的类加载及虚拟机执行子系统的知识去实现在服务端执行临时代码的能力。

1、目标

首先,在实现“在服务端执行临时代码”这个需求之前,先明确一下本次实战的具体目标,我们希望最终的产品是这样的:

(1)不依赖JDK版本,能在目前普通使用的JDK中部署。

(2)不改变原有服务端程序的部署,不依赖任何第三方类库。

(3)不侵入原有程序,即无需改变原程序的任何代码,也不会对原程序运行带来任何影响。

(4)“临时代码”应当具备足够的自由度,不需要依赖特定的类或特定的接口。

(5)“临时代码”的执行结果能返回客户端,执行结果可以包括程序中输出的信息及抛出的异常等。

2、思路

在程序实现的过程中,我们需要解决以下3个问题:

(1) 如何编译提交到服务器的Java代码?

(2) 如何执行编译后的Java代码?

(3) 如何收集Java代码的执行结果?

3、实现

第一个类用于实现“同一个类的代码可以被多次加载”这个需求,具体代码如下:

package org.swift.framework.RemotePlugin;

/**
 * 为了多次载入执行类而加入的加载器
 * 把defineClass方法开放出来,只有外部显式调用的时候才会使用到loadByte方法
 * 由虚拟机调用时,仍然按照原有的双亲委派规则使用loadClass方法进行加载
 * zww
 */
public class HotSwapClassLoader extends ClassLoader {

    public HotSwapClassLoader() {
        super(HotSwapClassLoader.class.getClassLoader()); //使用父类的加载器
    }

    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }

}

HotSwapClassLoader 所做的事情仅仅是公开父类中的defineClass() ,这个类加载器的类查找范围与它的父类加载器是完全一致的。

第二个类实现将 java.lang.System 替换为我们自己定义的HackSystem 类的过程,它直接修改符合Class 文件格式的 byte[] 数组中的常量池部分,将常量池中指定内容的 CONSTANT_Utf8_info 常量替换为新的字符串。

package org.swift.framework.RemotePlugin;

/**
 * 修改Class文件,暂时只提供修改常量池常量的功能
 */
public class ClassModifier {

    /**
     * Class文件中常量池的起始偏移
     */
    private static final int CONSTANT_POOL_COUNT_INDEX = 8;

    /**
     * CONSTANT_Utf8_info 常量的tag标志
     */
    private static final int CONSTANT_Utf8_info = 1;

    /**
     * 常量池中11种常量所占的长度,CONSTANT_Utf8_info型常量除外,因为不是定长的
     */
    private static final int[] CONSTANT_ITEM_LENGTH = {-1, -1, -1, 5, 5, 9, 9, 3, 3, 5, 5, 5, 5};

    private static final int u1 = 1;
    private static final int u2 = 2;

    private byte[] classByte;

    public ClassModifier(byte[] classByte) {
        this.classByte = classByte;
    }

    public byte[] modifyUTF8Constant(String oldStr, String newStr) {
        int cpc = getConstantPoolCount();   //常量的数量
        int offset = CONSTANT_POOL_COUNT_INDEX + u2;    //CONSTANT_POOL 起始位置
        for (int i = 0; i < cpc; i++) {
            int tag = ByteUtils.bytes2Int(classByte, offset, u1);   //获取常量型
            if (tag == CONSTANT_Utf8_info) {    //判断常量型类型是否是CONSTANT_Utf8_info
                int len = ByteUtils.bytes2Int(classByte, offset + u1, u2);
                offset += (u1 + u2);
                String str = ByteUtils.bytes2String(classByte, offset, len);
                if (str.equalsIgnoreCase(oldStr)) {
                    byte[] strBytes = ByteUtils.string2Bytes(newStr);
                    byte[] strLen = ByteUtils.int2Bytes(newStr.length(), u2);
                    classByte = ByteUtils.bytesReplace(classByte, offset - u2, u2, strLen);
                    classByte = ByteUtils.bytesReplace(classByte, offset, len, strBytes);
                    return classByte;
                } else {
                    offset += len;
                }
            } else {
                offset += CONSTANT_ITEM_LENGTH[tag];
            }
        }
        return classByte;
    }

    /**
     * 获取常量池中常量的数量
     * @return
     */
    private int getConstantPoolCount() {
        return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
    }

}

ByteUtils 工具的实现:

package org.swift.framework.RemotePlugin;

public class ByteUtils {

    public static int bytes2Int(byte[] b, int start, int len) {
        int sum = 0;
        int end = start + len;
        for (int i = start; i < end; i++) {
            // 因为当系统检测到byte可能会转化成int或者说byte与int类型进行运算的时候,
            // 就会将byte的内存空间高位补1(也就是按符号位补位)扩充到32位
            // 如果b[i]为负数时:例如:10000001 & 11111111  ==》 1111111111111111111111111 10000001 & 11111111 = 000000000000000000000000 10000001
            int n = ((int)b[i]) & 0xff;
            n <<= (--len) * 8;
            sum = n + sum;
        }
        return sum;
    }

    public static byte[] int2Bytes(int value, int len) {
        byte[] b = new byte[len];
        for (int i = 0; i < len; i++) {
            b[len - i - 1] = (byte) ((value >> 8 * i) & 0xff);
        }
        return b;
    }

    public static String bytes2String(byte[] b, int start, int len) {
        return new String(b, start, len);
    }

    public static byte[] string2Bytes(String str) {
        return str.getBytes();
    }

    public static byte[] bytesReplace(byte[] originalBytes, int offset, int len, byte[] replaceBytes) {
        // ||        |~offset|    ~len   ||      ||
        byte[] newBytes = new byte[originalBytes.length - len + replaceBytes.length];
        System.arraycopy(originalBytes, 0, newBytes, 0, offset); //替换位置之前
        System.arraycopy(replaceBytes, 0, newBytes, offset, replaceBytes.length); //替换的位置
        System.arraycopy(originalBytes, offset + len, newBytes, offset + replaceBytes.length, originalBytes.length - offset -len); //替换的位置之后
        return newBytes;
    }

}

经过ClassModifier 处理后的 byte[] 数据才会传给 HotSwapClassLoader.loadByte() 方法进行类加载

最后一个类就是前面提到的代替 java.lang.System 的 HackSystem ,这个类除了把 out 和 err 两个静态变量修改了,其他都来自于 System类的 public方法。

package org.swift.framework.RemotePlugin;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.PrintStream;

/**
 * 为JavaClass 劫持 java.lang.System 提供支持
 * 除了 out 和 err 外,其余的都直接转发给 System 处理
 */
public class HackSystem {

    public final static InputStream in = System.in;

    private static ByteArrayOutputStream buffer = new ByteArrayOutputStream();

    public final  static PrintStream out = new PrintStream(buffer);

    public final static  PrintStream err = out;

    public static String getBufferString() {
        return buffer.toString();
    }

    public static void clearBuffer() {
        buffer.reset();
    }

    public static void setSecurityManager(final SecurityManager s) {
        System.setSecurityManager(s);
    }

    public static SecurityManager getSecurityManager() {
        return System.getSecurityManager();
    }

    public static long currentTimeMillis() {
        return System.currentTimeMillis();
    }

    //下面所有的方法都与 java.lang.System 的名称一样
    //实现都是字节调System的对应方法
    //因版面原因,省略其他方法

}

至此,4个支持类已经讲解完毕,我们来看看最后一个类 JavaClassExecuter ,它是提供外部调用的入口

package org.swift.framework.RemotePlugin;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * JavaClass 执行工具
 */
public class JavaClassExecuter {

    /**
     * 执行外部传过来的代表一个Java类的byte数组
     * 将输入类byte数组中代表 java.lang.System的CONTANT_Utf8_info常量修改为劫持后的HackSystem类
     * 执行方法为该类的 main 方法,输出结构为该类向System.out/err输出的信息
     * @param classByte
     * @return
     */
    public static String execute(byte[] classByte) {
        HackSystem.clearBuffer();
        ClassModifier classModifier = new ClassModifier(classByte);
        //修改Class字节码,把HackSystem 替代 System
        byte[] modiBytes = classModifier.modifyUTF8Constant("java.lang.System", "org.swift.framework.RemotePlugin.HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modiBytes);
        try {
            //调用其main方法
            Method method = clazz.getMethod("main", new Class[] { String[].class});
            method.invoke(null, new String[] {null});
        } catch (Exception e) {
            e.printStackTrace(HackSystem.out);
        }
        return HackSystem.getBufferString();
    }

}

4、验证

任意写一个Jaca类,只需要向外System.out 信息即可,同事放到指定路径 C://TestClass.class ,然后建立一个Jsp 文件,在浏览器可以看到这个类的运行结果。

package org.swift.framework.RemotePlugin;

public class TestClass {
    public static void main(String[] args) {
        System.out.println("this is a test class");
    }
}
<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.swift.framework.RemotePlugin.*" %>
<%
    InputStream is = new FileInputStream("C:/TestClass.class");
    byte[] b = new byte[is.available()];
    is.read(b);
    is.close();

    out.println("<textarea style='width:1000;height:800'>");
    out.println(JavaClassExecuter.execute(b));
    out.println("</textarea>");
%>

其中主要需要学习的就是对 class文件的内容进行修改替换,并可以正常提供使用。

三、总结

本书中第6~9章介绍了class文件格式,类加载及虚拟机执行引擎及部分内容,这些内容时虚拟机必不可少的组成部分,了解了虚拟机如何执行程序,才能更好的理解怎样才能写出优秀的代码。

关于虚拟机执行子系统的介绍到此为止就结束了,通过这4章的讲解,我们描绘了一个虚拟机应该如何运行class文件的概念模型。对于具体到某个虚拟机的实现,为了使实现简单清晰,或者为了更快的运行速度,在虚拟机内部的运作跟概念模型可能会有非常大的差异,但从最终的执行结果来看应该是一致的。

 

推荐博文

素小暖讲JVM

 

鸣谢:特别感谢作者周志明提供的技术支持!

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!