使用Java代理仅快速转换一个类
我想在不增加大量开销的情况下测量服务器的启动时间 我实际上想要测量的是从服务器进程执行到服务器开始侦听已知端口的时间 例如,我想测量一个简单的Netty服务器的启动时间。i、 e.从启动到准备接受请求的时间 我使用Byte Buddy开发了一个Java代理使用Java代理仅快速转换一个类,java,instrumentation,javassist,javaagents,byte-buddy,Java,Instrumentation,Javassist,Javaagents,Byte Buddy,我想在不增加大量开销的情况下测量服务器的启动时间 我实际上想要测量的是从服务器进程执行到服务器开始侦听已知端口的时间 例如,我想测量一个简单的Netty服务器的启动时间。i、 e.从启动到准备接受请求的时间 我使用Byte Buddy开发了一个Java代理 public class Agent { public static void premain(String arg, Instrumentation instrumentation) { new AgentBuilder.Defa
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()));
}
}
使用此代理,启动时间约为1400ms。但是,当我通过修改服务器代码来测量启动时间时,服务器的启动时间大约为650ms
因此,在考虑启动时间时,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毫秒
如何将开销降至最低并测量启动时间?有没有一种方法可以直接转换一个特定的类而不必遍历所有的类?如果我可以直接转换一个类,我认为我应该能够尽可能地减少开销。因为您在一个
premain
中,您可能会测量许多以前未使用过的类的加载和初始化时间。在应用程序首次使用这些类时,很可能会在稍后的时间加载并初始化大量此类,而不作为“启动时间”进行度量,因此将此时间移到度量的启动时间可能不是一个实际问题
请注意,在这两个变体中都使用lambda表达式,这会导致JRE提供的后端初始化。在OpenJDK的情况下,它在引擎盖下使用ASM,但由于它已被重新打包以避免与使用ASM的应用程序发生冲突,因此Buddy内部使用的类与Buddy内部使用的类不同,因此您需要付出两次初始化ASM的代价
如前所述,如果仍将使用这些类,即如果应用程序稍后将使用lambda表达式或方法引用,则不必担心这一点,因为“优化”只会将初始化转移到稍后的时间。但是,如果应用程序不使用lambda表达式或方法引用,或者您希望不惜一切代价从测量的启动时间中删除此时间跨度,则可以求助于普通接口实现,使用内部类或让代理实现接口
为了进一步缩短启动时间,您可以直接使用ASM,跳过字节伙伴类的初始化,例如
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());
}
}
导入java.lang.instrument.*;
导入java.lang.management.ManagementFactory;
导入java.security.ProtectionDomain;
导入org.objectweb.asm.*;
导入org.objectweb.asm.commons.AdviceAdapter;
公共类代理扩展ClassVisitor实现ClassFileTransformer{
私有静态最终字符串NETTY_CLASS=“io/NETTY/bootstrap/AbstractBootstrap”;
公共静态void premain(字符串arg,指令插入){
instrumentation.addTransformer(新代理());
}
公职人员(){
超级(操作码ASM5);
}
公共字节[]转换(类加载器、字符串类名称、类cl、,
ProtectionDomain pd,字节[]classfileBuffer){
if(!NETTY_CLASS.equals(className))返回null;
ClassReader cr=新的ClassReader(classfileBuffer);
ClassWriter cw=新的ClassWriter(cr,0);
已同步(此){
super.cv=cw;
试试{cr.accept(this,0);}
最后{super.cv=null;}
}
返回cw.toByteArray();
}
@凌驾
公共方法访客访问方法(
int访问、字符串名称、字符串描述、字符串签名、字符串[]异常){
MethodVisitor mv=super.visitMethod(访问、名称、描述、签名、异常);
if(name.equals(“绑定”)
&&desc.equals(((Ljava/net/SocketAddress;)Lio/netty/channel/ChannelFuture;){
返回新的AdviceAdapter(操作码.ASM5,mv,访问,名称,描述){
@凌驾
MethodExit上受保护的void(int操作码){
super.visitMethodInsn(操作码.INVOKESTATIC,
Agent.class.getName().replace('.','/'),
“注射法”,“V”,假);
super.onMethodExit(操作码);
}
};
}
返回mv;
}
公共静态void injectedMethod(){
System.out.printf(“服务器已启动。当前时间(毫秒):%d”,
System.currentTimeMillis());
System.out.printf(“服务器已启动。当前正常运行时间(毫秒):%d”,
ManagementFactory.getRuntimeMXBean().getUptime());
}
}
(未测试)
显然,这段代码比使用Byte-Buddy的代码更复杂,因此您必须决定进行哪种权衡
ASM已经非常轻量级了。更深入的研究意味着只使用
ByteBuffer
和HashMap
进行类文件转换;这是可能的,但肯定不是你想走的路。因为你在一个premain
中,你可能会测量很多以前没有使用过的类的加载和初始化时间。在应用程序首次使用这些类时,很可能会在稍后的时间加载并初始化大量此类,而不作为“启动时间”进行度量,因此将此时间移到度量的启动时间可能不是一个实际问题
请注意,在这两个变体中都使用lambda表达式,这会导致JRE提供的后端初始化。在OpenJDK的情况下,它在引擎盖下使用ASM,但是因为它已经被重新打包以避免冲突