使用Java代理

时间:2018-01-03 06:19:16

标签: java instrumentation javassist javaagents byte-buddy

我想测量服务器的启动时间而没有相当大的开销。

我真正想要衡量的是从服务器进程执行到服务器开始侦听知名端口的时间。

例如,我想测量一个简单的Netty Server的启动时间。即从启动到准备接受请求的时间。

我使用Byte-Buddy开发了一个Java代理。

public class Agent {

public static void premain(String arg, Instrumentation instrumentation) {
    new AgentBuilder.Default()
            .type(ElementMatchers.named("io.netty.bootstrap.AbstractBootstrap"))
            .transform((builder, typeDescription, classLoader, javaModule) ->
                    builder.visit(Advice.to(TimeAdvice.class)
                            .on(ElementMatchers.named("bind").and(ElementMatchers.takesArguments(SocketAddress.class)))))
            .installOn(instrumentation);
}
}

以下是TimeAdvice的源代码

public class TimeAdvice {

@Advice.OnMethodExit
static void exit(@Advice.Origin String method) {
    System.out.println(String.format("Server started. Current Time (ms): %d", System.currentTimeMillis()));
    System.out.println(String.format("Server started. Current Uptime (ms): %d",
            ManagementFactory.getRuntimeMXBean().getUptime()));
}
}

使用此代理,启动时间约为1400毫秒。但是,当我通过修改服务器代码来测量启动时间时,服务器的启动时间大约为650毫秒。

因此,在考虑启动时间时,看起来像byte-buddy Java代理会有相当大的开销。

我还尝试了另一个带Javassist的Java代理。

public class Agent {

private static final String NETTY_CLASS = "io/netty/bootstrap/AbstractBootstrap";

public static void premain(String arg, Instrumentation instrumentation) {
    instrumentation.addTransformer((classLoader, s, aClass, protectionDomain, bytes) -> {
        if (NETTY_CLASS.equals(s)) {
            System.out.println(aClass);
            long start = System.nanoTime();
            // Javassist
            try {
                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get("io.netty.bootstrap.AbstractBootstrap");
                CtMethod m = cc.getDeclaredMethod("bind", new CtClass[]{cp.get("java.net.SocketAddress")});
                m.insertAfter("{ System.out.println(\"Server started. Current Uptime (ms): \" + " +
                        "java.lang.management.ManagementFactory.getRuntimeMXBean().getUptime());}");
                byte[] byteCode = cc.toBytecode();
                cc.detach();
                return byteCode;
            } catch (Exception ex) {
                ex.printStackTrace();
            } finally {
                System.out.println(String.format("Agent - Transformation Time (ms): %d", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)));
            }
        }

        return null;
    });
}
}

使用此代理,启动时间约为800毫秒。

如何最大限度地减少开销并测量启动时间?有没有办法直接转换特定的类而不通过所有类?如果我可以直接转换一个类,我想我应该能够尽可能地减少开销。

1 个答案:

答案 0 :(得分:2)

由于您处于premain,因此您可能会测量之前未使用过的许多类的加载和初始化时间。完全可能的是,在应用程序第一次使用它们时,无论如何都会加载和初始化大量这些类,而不是作为“启动时间”进行测量,因此将此时间转移到测量的启动时间时间可能不是一个实际问题。

请注意,您在两个变体中都使用lambda表达式,这会导致JRE提供的后端初始化。在OpenJDK的情况下,它使用ASM,但由于它已被重新打包以避免与使用ASM的应用程序发生冲突,因此Byte-Buddy在内部使用的类别不同,因此您需要支付两次初始化ASM的费用

如上所述,如果这些类仍然会被使用,即如果应用程序将在以后使用lambda表达式或方法引用,你不应该担心这一点,因为“优化”它只会将初始化转移到以后的时间。但是如果应用程序没有使用lambda表达式或方法引用,或者你想不惜一切代价从测量的启动时间中删除这个时间跨度,你可以使用普通的接口实现,使用内部类或让Agent实现界面。

为了进一步减少启动时间,您可以直接使用ASM,跳过Byte-Buddy类的初始化,例如

import java.lang.instrument.*;
import java.lang.management.ManagementFactory;
import java.security.ProtectionDomain;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;

public class Agent extends ClassVisitor implements ClassFileTransformer {
    private static final String NETTY_CLASS = "io/netty/bootstrap/AbstractBootstrap";

    public static void premain(String arg, Instrumentation instrumentation) {
        instrumentation.addTransformer(new Agent());
    }

    public Agent() {
        super(Opcodes.ASM5);
    }
    public byte[] transform(ClassLoader loader, String className, Class<?> cl,
                            ProtectionDomain pd, byte[] classfileBuffer) {
        if(!NETTY_CLASS.equals(className)) return null;

        ClassReader cr = new ClassReader(classfileBuffer);
        ClassWriter cw = new ClassWriter(cr, 0);
        synchronized(this) {
            super.cv = cw;
            try { cr.accept(this, 0); }
            finally { super.cv = null; }
        }
        return cw.toByteArray();
    }

    @Override
    public MethodVisitor visitMethod(
        int access, String name, String desc, String signature, String[] exceptions) {

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if(name.equals("bind")
        && desc.equals("(Ljava/net/SocketAddress;)Lio/netty/channel/ChannelFuture;")) {
            return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
                @Override
                protected void onMethodExit(int opcode) {
                    super.visitMethodInsn(Opcodes.INVOKESTATIC,
                        Agent.class.getName().replace('.', '/'),
                        "injectedMethod", "()V", false);
                    super.onMethodExit(opcode);
                }
            };
        }
        return mv;
    }
    public static void injectedMethod() {
        System.out.printf("Server started. Current Time (ms): %d",
                          System.currentTimeMillis());
        System.out.printf("Server started. Current Uptime (ms): %d",
                          ManagementFactory.getRuntimeMXBean().getUptime());
    }
}

(未经测试)

显然,这段代码比使用Byte-Buddy的代码更复杂,所以你必须决定做出哪些权衡。

ASM已经非常轻巧。更深入的意思是仅使用ByteBufferHashMap进行类文件转换;这是可能的,但肯定不是你想去的路......