IDEA 的 debug 怎么实现?出于这个好奇心,我越挖越深!

instrument 是 JVM 提供的一个可以修改已加载类文件的类库。1.6以前,instrument 只能在 JVM 刚启动开始加载类时生效,之后,instrument 更是支持了在运行时对类定义的修改。

使用

要使用 instrument 的类修改功能,我们需要实现它的 ClassFileTransformer 接口定义一个类文件转换器。它唯一的一个 transform() 方法会在类文件被加载时调用,在 transform 方法里,我们可以对传入的二进制字节码进行改写或替换,生成新的字节码数组后返回,JVM 会使用 transform 方法返回的字节码数据进行类的加载。

JVM TI

定义完了字节码的修改和重定义方法,但我们怎么才能让 JVM 能够调用我们提供的类转换器呢?这里又要介绍到 JVM TI 了。

介绍

JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一个非常强大的对 JVM 操作的工具接口,通过这个接口,我们可以实现对 JVM 多种组件的操作,从JVMTM Tool Interface 这里我们认识到 JVM TI 的强大,它包括了对虚拟机堆内存、类、线程等各个方面的管理接口。

JVM TI 通过事件机制,通过接口注册各种事件勾子,在 JVM 事件触发时同时触发预定义的勾子,以实现对各个 JVM 事件的感知和反应。

Agent

Agent 是 JVM TI 实现的一种方式。我们在编译 C 项目里链接静态库,将静态库的功能注入到项目里,从而才可以在项目里引用库里的函数。我们可以将 agent 类比为 C 里的静态库,我们也可以用 C 或 C++ 来实现,将其编译为 dll 或 so 文件,在启动 JVM 时启动。

这时再来思考 Debug 的实现,我们在启动被 Debug 的 JVM 时,必须添加参数 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 选项就指定了我们要加载的 Java Agent,jdwp 是 agent 的名字,在 linux 系统中,我们可以在 jre 目录下找到 jdwp.so 库文件。

Java 的调试体系 jdpa 组成,从高到低分别为 jdi->jdwp->jvmti,我们通过 JDI 接口发送调试指令,而 jdwp 就相当于一个通道,帮我们翻译 JDI 指令到 JVM TI,最底层的 JVM TI 最终实现对 JVM 的操作。

使用

JVM TI 的 agent 使用很简单,在启动 agent 时添加 -agent 参数指定我们要加载的 agent jar包即可。

而要实现代码的修改,我们需要实现一个 instrument agent,它可以通过在一个类里添加 premain() 或 agentmain() 方法来实现。而要实现 1.6 以上的动态 instrument 功能,实现 agentmain 方法即可。

在 agentmain 方法里,我们调用 Instrumentation.retransformClasses() 方法实现对目标类的重定义。

另外往一个正在运行的 JVM 里动态添加 agent,还需要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包里包含的 VirtualMachine 类提供了 attach 一个本地 JVM 的功能,它需要我们传入一个本地 JVM 的 pid, tools.jar 可以在 jre 目录下找到。推荐:Intellij IDEA中竟然还有这么多炫酷的插件

agent生成

另外,我们还需要注意 agent 的打包,它需要指定一个 Agent-Class 参数指定我们的包括 agentmain 方法的类,可以算是指定入口类吧。

此外,还需要配置 MANIFEST.MF 文件的一些参数,允许我们重新定义类。如果你的 agent 实现还需要引用一些其他类库时,还需要将这些类库都打包到此 jar 包中,下面是我的 pom 文件配置。

        

org.apache.maven.plugins

maven-assembly-plugin

        

asm.T
estAgent

true

true

1.0

all-permissions

        

jar-with-dependencies

              

另外在打包时需要使用 mvn assembly:assembl 命令生成 jar-with-dependencies 作为 agent。

代码实现

我在测试时写了一个用以上技术实现了一个简单的字节码动态修改的 Demo。

被修改的类

TransformTarget 是要被修改的目标类,正常执行时,它会三秒输出一次 “hello”。

public class TransformTarget {

public static void main(String[] args) {

while (true) {

try {

Thread.sleep(3000L);

} catch (Exception e) {

break;

}

printSomething();

}

}

public static void printSomething() {

System.out.println(“hello”);

}

}

Agent

Agent 是执行修改类的主体,它使用 ASM 修改 TransformTarget 类的方法,并使用 instrument 包将修改提交给 JVM。

入口类,也是代理的 Agent-Class。

public class TestAgent {

public static void agentmain(String args, Instrumentation inst) {

inst.addTransformer(new TestTransformer(), true);

try {

inst.retransformClasses(TransformTarget.class);

System.out.println(“Agent Load Done.”);

} catch (Exception e) {

System.out.println(“agent load failed!”);

}

}

}

执行字节码修改和转换的类。

public class TestTransformer implements ClassFileTransformer {

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

System.out.println("Transforming " + className);

ClassReader reader = new ClassReader(classfileBuffer);

ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter);

reader.accept(classVisitor, ClassReader.SKIP_DEBUG);

return classWriter.toByteArray();

}

class TestClassVisitor extends ClassVisitor implements Opcodes {

TestClassVisitor(int api, ClassVisitor classVisitor) {

super(api, classVisitor);

}

@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(“printSomething”)) {

mv.visitCode();

Label l0 = new Label();

mv.visitLabel(l0);

mv.visitLineNumber(19, l0);

mv.visitFieldInsn(Opcodes.GETSTATIC, “java/lang/System”, “out”, “Ljava/io/PrintStream;”);

mv.visitLdcInsn(“bytecode replaced!”);

mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, “java/io/PrintStream”, “println”, “(Ljava/lang/String;)V”, false);

Label l1 = new Label();

mv.visitLabel(l1);

mv.visitLineNumber(20, l1);

mv.visitInsn(Opcodes.RETURN);

mv.visitMaxs(2, 0);

mv.visitEnd();

TransformTarget.printSomething();

}

return mv;

}

}

}

Attacher

使用 tools.jar 里方法将 agent 动态加载到目标 JVM 的类。

public class Attacher {

public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {

VirtualMachine vm = VirtualMachine.attach(“34242”); // 目标 JVM pid

vm.loadAgent(“/path/to/agent.jar”);

}

}

这样,先启动 TransformTarget 类,获取到 pid 后将其传入 Attacher 里,并指定 agent jar,将 agent attach 到 TransformTarget 中,原来输出的 “hello” 就变成我们想要修改的 “bytecode replaced!” 了。

图片

小结

掌握了字节码的动态修改技术后,再回头看 Btrace 的原理就更清晰了,稍微摸索一下我们也可以实现一个简版的。另外很多大牛实现的各种 Java 性能分析工具的技术栈也不外如此,了解了这些,未来我们也可以写出适合自己的工具,至少能对别人的工具进行修改~

不得不说 Java 的生态真的非常繁荣,当真是博大精深,查阅一个模块的资料时能总引出一大堆新的概念,永远有学不完的新东西。

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值