入侵JVM?Java Agent原理浅析和实践(下)

声明:本文首发于京东零售技术公众号,为博主本人撰写投稿。

运行时修改字节码

了解到上述机制以后,我们可以通过在目标JVM运行时对其中的类进行重新定义,做到运行时插桩代码。

我们知道ASM是一个字节码修改框架,因此就可以在类转换器中,对原本类的字节码进行修改,然后再对这个类进行重定义(retransform)。

首先我们实现ClassFileTransformer接口,前文中在transform方法中并没有对于字节码进行修改,只是单纯的打印了一些信息,既然需要对字目标类的节码进行修改,我们需要了解下ClassFileTransformer接口中唯一需要实现的方法transform,方法签名如下:

    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;

可以看到方法入参有该类的类加载器、类名、类Class对象、类的保护域、以及最重要的classfileBuffer,也就是这个类的字节码,此时就可以借助ASM这个字节码大杀器来为所欲为了。现在我们实现一个字节的类转换器MyClassTransformer,然后使用ASM来对字节码进行修改。

public class MyClassTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

        // 对类字节码进行操作
        // 这里需要注意,不能对classfileBuffer这个数组进行修改操作
        try {
            // 创建ASM ClassReader对象,导入需要增强的对象字节码
            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            
            // 自己实现的代码增强器
            MyEnhancer myEnhancer = new MyEnhancer(classWriter);

            // 增强字节码
            reader.accept(myEnhancer, ClassReader.SKIP_FRAMES);

            // 返回MyEnhancer增强后的字节码
            return classWriter.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // return null 则不会对类进行转换
        return null;
    }
}

至此,我们拼上了JVM运行时插桩代码的最后一块拼图,这样就可以理解Arthas这类基于Java Agent的性能分析工具是如何在JVM运行时对你的代码进行了修改。

在这里插入图片描述
接着实现一个字节码增强器,借助ASM将对方法入参和方法耗时的监控代码织入,这里需要对字节码有一定了解,这里笔者使用到ASM提供的AdviceAdapter类简化开发。

public class MyEnhancer extends ClassVisitor implements Opcodes {

    public MyEnhancer(ClassVisitor classVisitor) {
        super(ASM7, classVisitor);
    }

    /**
     * 对字节码中的方法定义进行修改
     */
    @Override
    public MethodVisitor visitMethod(int access, final String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (isIgnore(mv, access, name)) {
            return mv;
        }
        return new AdviceAdapter(Opcodes.ASM7, new JSRInlinerAdapter(mv, access, name, descriptor, signature, exceptions), access, name, descriptor) {

            private final Type METHOD_CONTAINER = Type.getType(MethodContainer.class);
            private int timeIdentifier;
            private int argsIdentifier;

            /**
             * 进入方法前
             */
            @Override
            protected void onMethodEnter() {
                // 调用System.nanoTime()方法,将方法出参推入栈顶
                invokeStatic(Type.getType(System.class), Method.getMethod("long nanoTime()"));
                // 构造一个Long类型的局部变量,然后返回这个变量的标识符
                timeIdentifier = newLocal(Type.LONG_TYPE);

                // 存储栈顶元素也就是System.nanoTime()返回值,到指定位置本地变量区
                storeLocal(timeIdentifier);

                // 加载入参数组,将入参数组ref推入栈顶
                loadArgArray();
                // 构造一个Object[]类型的局部变量,返回这个变量的标识符
                argsIdentifier = newLocal(Type.getType(Object[].class));
                // 存储入参到指定位置本地变量区
                storeLocal(argsIdentifier);
            }

            @Override
            protected void onMethodExit(int opcode) {
                // 加载指定位置的本地变量到栈顶
                loadLocal(timeIdentifier);
                loadLocal(argsIdentifier);
                // 相当于调用MethodContainer.showMethod(long, Object[])方法
                invokeStatic(METHOD_CONTAINER, Method.getMethod("void showMethod(long,Object[])"));
            }

        };
    }

    /**
     * 方法是否需要被忽略(静态构造函数和构造函数)
     */
    private boolean isIgnore(MethodVisitor mv, int access, String methodName) {
        return null == mv
                || isAbstract(access)
                || isFinalMethod(access)
                || "<clinit>".equals(methodName)
                || "<init>".equals(methodName);
    }

    private boolean isAbstract(int access) {
        return (ACC_ABSTRACT & access) == ACC_ABSTRACT;
    }

    private boolean isFinalMethod(int methodAccess) {
        return (ACC_FINAL & methodAccess) == ACC_FINAL;
    }

}

由于这里对于字节码的修改是在方法内部,那么实现一些复杂逻辑的最好方式,就是调用外部类的静态方法,虚拟机字节码指令中的invokestatic 是调用指定类的静态方法的指令,这里我们将方法开始时间和方法入参作为参数调用MethodContainer.showMethod 方法,方法实现如下:

public class MethodContainer {

    // 实现静态方法
    public static void showMethod(long startTime, Object[] Args) {
        System.out.println("方法耗时:" + (System.nanoTime() - startTime) / 1000000 + "ms, 方法入参:" + Arrays.toString(Args));
    }

}

ASM操作字节码需要一定的学习才能理解,如果把上述字节码增强前后用Java代码表示大体入下:

    // ASM代码增强前
    public void test(int x) throws InterruptedException {
        Thread.sleep(2000L);
        System.out.println("i'm working " + x);
    }

    // ASM代码增强后
    public void test(int x) throws InterruptedException {
        long var2 = System.nanoTime();
        Object[] var4 = new Object[]{new Integer(x)};
        Thread.sleep(2000L);
        System.out.println("i'm working " + x);
        MethodContainer.showMethod(var2, var4);
    }

最后运行AttachUitl,可以看到正在运行中的JVM被成功的插入了我们实现的字节码,对于目标虚拟机来说是完全不需要任何实现的,而且被重定义的代码也可以被还原,感兴趣的同学可以自己了解下。

在这里插入图片描述

总结

对于Java开发者来说,代码插桩是很熟悉的一个概念,而且目前也有很多成熟的方式可以完成,比如说Spring AOP实现采用的动态代理方式,Lombok采用的插入式注解处理器方式等。

动态代理JSR269-插入式注解处理器Instrument Agent
插入时间运行时代码编译期随时
性能生成新类,有性能开销运行无影响重定义类时JVM会STW
功能性针对方法层面需要定义注解,有局限性局限性最小
易用性纯Java代码开发需要学习API需要了解字节码开发
实现Spring AOP,AspectJ等Lombok等链路追踪、Arthas等
侵入性VM运行时不可卸载VM运行时不可卸载可卸载

所谓术业有专攻,Instrument Agent虽然强大,但也不见得适用所有的场景,对于日志统计、方法监控,动态代理已经能很好的满足这方面的需求,但是对于JVM性能监控或方法实时运行分析,Instrument Agent可以随时插入、随时卸载、随时修改的特性就体现出了极大的优点,同时其基于Java代码开发又会相应的降低一些开发难度,这也是业内很多性能分析软件选择这种方式实现的原因。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值