JVM类加载以及执行的实战

前几篇文章主要是去理解JVM类加载的原理和应用,这一回讲一个可以自己动手的例子,希望能从头到尾的理解类加载以及执行的整个过程。

这个例子是从周志明的著作《深入理解Java虚拟机》第9章里抄来的。原作者因为有丰富的经验,可以站在一个很高的高度去描述整个过程。而我只能以现有的水平,简单的理解这个例子。

如果读者感觉不错,那都是原作者的智慧;如果觉得不过尔尔,那就是我水平有限。

先说说日志。原先,我特别不喜欢在自己的程序里输出日志。写的时候那叫一个爽,可是一旦运行出错,那就麻烦了。因为不知道具体执行到哪一步出的错,所以就要调试一大片代码。尤其是大的项目,是要经常去分析日志的。所以,我们都尽量在代码里输出详细的日志。

但是,我们不可能把所有的情况考虑到。也就是说,当程序在服务器上跑的时候,我们想查看某个运行时的状态和数据,如果没有日志输出,就无能为力。

当然,并不是真的无能为力。这篇文章就是教你一些思考,以及解决这个问题的一个思路。

说白了,要是服务器能够临时去执行一段代码,输出日志,问题迎刃而解。有了前面类加载的知识,我们应该会想到:我们自己写一个类,然后动态加载到服务器的JVM进程的方法区,最后反射调用输出日志的那个方法。

但是,仔细想想,需要考虑的事情还有许多:

1)这个类可能会经常的被修改,经常的被加载,所以,执行完之后,要能够从方法区卸载。而能够被卸载的条件之一,就是它的类加载器被回收。之前已经加载了多个类的类加载器,是不可能那么快被回收的。所以,这里要自定义一个类加载器去加载待执行的类。

2)待执行的类要能够访问原来项目里的类,比如说WEB-INF下面的那些类。那怎么办呢?就要用到双亲委派模型了,将自定义类加载器的父类加载器设置为加载这个类加载器的类加载器。听起来有点绕,没关系,直接上代码

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

    public HotSwapClassLoader() {
        // 设置父类加载器,用以访问JVM进程中的原来的类
        super(HotSwapClassLoader.class.getClassLoader());
    }

    /**
     * 加载待执行的类
     */
    public Class loadByte(byte[] classByte) {
        return defineClass(null, classByte, 0, classByte.length);
    }

}    

3)待执行类的方法里面的日志输出到哪里?你可能脱口而出,System.out.println()。但是System.out是标准输出,是整个JVM进程的资源,也不利于查看。也许,你会想通过System.setOut()指定一个文件作为输出。可是,一旦设定,那以后整个JVM进程的输出都会写到这个文件里面,这样就影响了原来的程序,这不是我们想要的。所以,我们必须写一个类来代替System类的作用。

/**
 * 为JavaClass劫持java.lang.System提供支持
 * 除了out和err外,其余的都直接转发给System处理
 *
 * @author zzm
 */
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();
    }

    public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length) {
        System.arraycopy(src, srcPos, dest, destPos, length);
    }

    public static int identityHashCode(Object x) {
        return System.identityHashCode(x);
    }

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

那就有人问了,既然能代替System类,就直接用这个类不就完了呗,也没有System类的事了?问得好,这就是下面第4点。

4)我们在客户端编写待执行类时,不能依赖特定的类;如果依赖了特定的类,就只有在能够访问到特定类的地方才能编译通过,受限制太多。也就是说,我们在写执行类时,不能用到HackSystem类,但是执行的时候,却又必须是HackSystem类。所以思路应该是这样的:在执行类里面输出时,还是用System.out,编译完成后,再去修改编译成的class文件,将常量池中java.lang.System这个符号替换成HackSystem。这里的难点是在程序中修改class文件,需要你特别熟悉class文件的每个数据项。

/**
 * 修改Class文件,暂时只提供修改常量池常量的功能
 * @author zzm
 */
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;
    }

    /**
     * 修改常量池中CONSTANT_Utf8_info常量的内容
     * @param oldStr 修改前的字符串
     * @param newStr 修改后的字符串
     * @return 修改结果
     */
    public byte[] modifyUTF8Constant(String oldStr, String newStr) {
        int cpc = getConstantPoolCount();
        int offset = CONSTANT_POOL_COUNT_INDEX + u2;
        for (int i = 0; i < cpc; i++) {
            int tag = ByteUtils.bytes2Int(classByte, offset, u1);
            if (tag == 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 常量池数量
     */
    public int getConstantPoolCount() {
        return ByteUtils.bytes2Int(classByte, CONSTANT_POOL_COUNT_INDEX, u2);
    }
}

/**
 * Bytes数组处理工具
 * @author
 */
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++) {
            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) {
        byte[] newBytes = new byte[originalBytes.length + (replaceBytes.length - len)];
        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;
    }
}

最后,来看看实现替换符号引用以及得到输出日志的类

/**
 * JavaClass执行工具
 *
 * @author zzm
 */
public class JavaClassExecuter {

    /**
     * 执行外部传过来的代表一个Java类的Byte数组<br>
     * 将输入类的byte数组中代表java.lang.System的CONSTANT_Utf8_info常量修改为劫持后的HackSystem类
     * 执行方法为该类的static main(String[] args)方法,输出结果为该类向System.out/err输出的信息
     * @param classByte 代表一个Java类的Byte数组
     * @return 执行结果
     */
    public static String execute(byte[] classByte) {
        HackSystem.clearBuffer();
        ClassModifier cm = new ClassModifier(classByte);
        byte[] modiBytes = cm.modifyUTF8Constant("java/lang/System", "org/fenixsoft/classloading/execute/HackSystem");
        HotSwapClassLoader loader = new HotSwapClassLoader();
        Class clazz = loader.loadByte(modiBytes);
        try {
            Method method = clazz.getMethod("main", new Class[] { String[].class });
            method.invoke(null, new String[] { null });
        } catch (Throwable e) {
            e.printStackTrace(HackSystem.out);
        }
        return HackSystem.getBufferString();
    }
}

传进来待执行类的class文件的字节数组,先将符号替换,然后加载该类,反射调用该类的main方法,最后将HackSystem类收集到的输出日志返回。

为了更直观的看到运行的结果,可以写一个jsp文件,通过浏览器去访问。

<%@ page import="java.lang.*" %>
<%@ page import="java.io.*" %>
<%@ page import="org.fenixsoft.classloading.execute.*" %>
<%
    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>");
%>

这里将待执行类TestClass.class放到服务器的C盘。只要TestClass里面main方法,有调用System.out,就可以将输出内容展现到页面上。我自己在Tomcat上面的项目里也测试了一把,现在把代码也贴出来

public class TestClass {
    public static void main(String[] args) {
        System.out.println("hello world!!!");
        ClassLoader cl = TestClass.class.getClassLoader();
        System.out.println("self: " + cl);
        while (cl.getParent() != null) {
            System.out.println(cl.getParent().getClass());
            cl = cl.getParent();
        }
    }
}

大家可以那我这个类去试一试,而且还可以根据输出结果去温习一下Tomcat的类加载体系。

整体流程讲完了,感觉还是很烧脑。不经意间,我们就充当了一回黑客,将系统类的调用变成了调用我们自己的逻辑。Java引入JVM的目的就是提高灵活性,可以动态的运行,但是也引入了一定的安全问题。

回想整个流程,其实也有可替代的方案。比如jdk1.6引入了动态编译,可以在运行时动态的编译和执行我们的待执行类,但还是依赖了特定类。

我这里只是抛砖引玉,推荐大家去看原作者的书,去看看更详细的讲解。

时间: 2024-08-29 02:09:10

JVM类加载以及执行的实战的相关文章

Java再学习-JVM类加载和执行机制

  JVM简介 JVM是Java Virtual Machine(Java虚拟机)的缩写,JVM是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的.Java虚拟机包括一套字节码指令集.一组寄存器.一个栈.一个垃圾回收堆和一个存储方法域. JVM屏蔽了与具体操作系统平台相关的信息,使Java程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行.JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器

深入理解JVM虚拟机6:深入理解JVM类加载机制

深入理解JVM类加载机制 简述:虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 下面我们具体来看类加载的过程: 类的生命周期 类从被加载到内存中开始,到卸载出内存,经历了加载.连接.初始化.使用四个阶段,其中连接又包含了验证.准备.解析三个步骤.这些步骤总体上是按照图中顺序进行的,但是Java语言本身支持运行时绑定,所以解析阶段也可以是在初始化之后进行的.以上顺序都只是说开始的顺序,实际过

JVM 类加载机制详解

原文出处: ziwenxie 如下图所示,JVM类加载机制分为五个部分:加载,验证,准备,解析,初始化,下面我们就分别来看一下这五个过程. 加载 加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的入口.注意这里不一定非得要从一个Class文件获取,这里既可以从ZIP包中读取(比如从jar包和war包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类). 验证

JVM类加载续

上一篇理解了JVM类加载过程的第一个阶段,这篇来说说剩下的阶段:验证.准备.解析.初始化.需要注意的是,这些阶段(解析除外)只是按照这个顺序开始,但是执行的过程中可能存在交叉. 验证:就是要对加载的二进制流文件进行各种检查,很好理解. 准备:为类变量(static)分配内存并设置初始值,即所谓的"零值",但是不包括常量(final). 解析:将常量池的符号引用替换成直接引用,这个阶段发生时间没有明确规定,但是有具体限制:在符号引用被使用之前,必须被解析. 上述3个阶段合称连接阶段. 初

JVM类加载机制(转)

原文出自:http://www.cnblogs.com/ityouknow/p/5603287.html 1.什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口. 类加载器并不需要等到某个类被"首次主动使用

JVM类加载机制概述:加载时机与加载过程

摘要: 我们知道,一个.java文件在编译后会形成相应的一个或多个Class文件,这些Class文件中描述了类的各种信息,并且它们最终都需要被加载到虚拟机中才能被运行和使用.事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的类加载机制.本文概述了JVM加载类的时机和生命周期,并结合典型案例重点介绍了类的初始化过程,揭开了JVM类加载机制的神秘面纱. 版权声明: 本文原创作者:书呆子Rico 作者

JVM总结(五):JVM字节码执行引擎

JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 基于栈的字节码解释执行引擎 基于栈的指令集与基于寄存器的指令集 JVM字节码执行引擎 虚拟机是相对于“物理机”而言的,这两种机器都有代码执行能力,其区别主要是物理机的执行引擎是直接建立在处理器.硬件.指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的.因此程序员可以自行制定指令集和执行引擎的

JVM类加载过程

JVM类加载过程 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载.验证.准备.解析.初始化.使用和卸载七个阶段.它们开始的顺序如下图所示: 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也成为动态绑定或晚期绑定).另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通

JVM系列(五) - JVM类加载机制详解

前言 本文将由浅及深,介绍Java类加载的过程和原理,进一步对类加载器的进行源码分析,完成一个自定义的类加载器. 正文 (一). 类加载器是什么 类加载器简言之,就是用于把.class文件中的字节码信息转化为具体的java.lang.Class对象的过程的工具. 具体过程: 在实际类加载过程中,JVM会将所有的.class字节码文件中的二进制数据读入内存中,导入运行时数据区的方法区中. 当一个类首次被主动加载或被动加载时,类加载器会对此类执行类加载的流程 – 加载.连接(验证.准备.解析).初始