使用JDI进行线上程序断点信息记录

之前在某一次蚂蚁金服的分享当中,得知可以通过在线上程序中进行断点记录,当程序运行到断点的时候,即将此次调用的上下文信息记录下来,以进行进一次分析。实际上这也是相当于在程序中打log记录信息的方式了。当线上出现问题时,我们最希望的就是将相应的参数信息记录下来,以拿到当时的数据,以便知道到底是哪儿出现了问题。但又不希望通过重新在代码中加入代码,然后发布这样的方式,因为新的发布可能导致问题重现机率变小等。

java的整个调试体系为JDPA,Oracle提供了高级的jdi接口以方便使用java来连接调试程序进行相应的调试。这样,只需要调用相应的java接口,就能进行打断点,记录断点,然后继续运行,清除断点这样基本的断点调试手法了。

1. 连接远程JVM

连接其它JVM称之为附加(attach)操作,当前实现中有2种,如果是本地JVM,则通过Process的方式即可,如果是远程,则需要通过socket的方式才能进行连接。首先是server端需要开启调试agent,并且指定相应的端口,如下启动命令所示:

-Xdebug -agentlib:jdwp=transport=dt_socket,address=1234,server=y,suspend=n

上面的参数表示监听指定端口(1234),并且当存在断点时并不主动阻塞。
旧的JVM也有使用Xrunjdwp参数的,但不再被建议使用.

客户端即通过相应的connector进行连接,如下的socket连接方式

VirtualMachineManager vmm = Bootstrap.virtualMachineManager();

List<AttachingConnector> connectors = vmm.attachingConnectors();
SocketAttachingConnector sac = null;
for(AttachingConnector ac : connectors) {
    if(ac instanceof SocketAttachingConnector) {
        sac = (SocketAttachingConnector) ac;
    }
}
assert sac != null;

Map<String, Connector.Argument> arguments = sac.defaultArguments();
Connector.Argument hostArg = arguments.get("hostname");
Connector.Argument portArg = arguments.get("port");

hostArg.setValue("127.0.0.1");
portArg.setValue(String.valueOf(1234));

vm = sac.attach(arguments);

即设置好主机地址,端口信息,然后即可以进行连接。

2. 开启新断点

相应的请求调用通过requestManager来完成,由刚才成功连接上的vm(VirtualMachine)来获取。由于这里是创建一个代码判断,因此需要获取相应的类,以及具体的断点位置,即相应的代码行。

要获取代码行,则要求源文件(class)中必须存在lineCode信息,即通过在抛出异常时,在异常信息中所指定的行数。这些信息默认情况下编译时会自动输出,但使用编译开关-g:none时,则不会输出。如果没有line信息,则不能创建相应的断点信息。创建信息如下所示:

eventRequestManager = vm.eventRequestManager();

ClassType clazz = (ClassType) vm.classesByName("A1").get(0);
Location location = clazz.locationsOfLine(15).get(0);

BreakpointRequest breakpointRequest
        = eventRequestManager.createBreakpointRequest(location);
breakpointRequest.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
breakpointRequest.enable();

以上即拿到类A1的第15行的执行段(可能有多个,一行代码可能有多个调用),并设置阻塞模式为线程阻塞,即只有当前线程被阻塞。最终启用之,即成功创建新断点。这里必须将其设置为阻塞,否则将断点记录产生时,由于当前线程继续运行了,将不能拿到相应的信息(表示为产生IncompatibleThreadStateException)

3. 记录断点信息

接下来就是从vm的事件队列中,不断地读取事件并处理断点记录事件即可。由于断点产生时,server端线程被阻塞,因此必须尽可能快地处理断相应的,以避免出现业务阻塞。标准的事件循环如下所示:

eventQueue = vm.eventQueue();
while(true) {
    eventSet = eventQueue.remove();
    EventIterator eventIterator = eventSet.eventIterator();
    while(eventIterator.hasNext()) {
        Event event = eventIterator.next();
        execute(event);
    }

    eventSet.resume();
}

上面在事件记录处理完之后,必须将相应线程resume,表示继续运行。

这里获取的event为一个抽象的事件记录,可以通过类型判断转型为具体的事件,这里我们转型为BreakpointEvent,即断点记录,并通过断点处的线程拿到线程帧,进而获取相应的变量信息,并打印记录。如下所示:

BreakpointEvent breakpointEvent = (BreakpointEvent) event;
ThreadReference threadReference = breakpointEvent.thread();
StackFrame stackFrame = threadReference.frame(0);
List<LocalVariable> localVariables = stackFrame
        .visibleVariables();
localVariables.forEach(t -> {
    Value value = stackFrame.getValue(t);
    System.out.println("local->" + value.type() + "," + value.getClass() + "," + value);
});

上面的value也是抽象形式,也可以通过类型转换以打印不同类型的对象。

4. 断点继续运行

如第2点所示,必须调用相应的event.resume

5. 清除断点

如果记录的信息又足够(如记录了半天的数据了),则可能通过requestManager.deleteEventRequest调用相应的断点,或者调用deleteAllBreakpoints删除所有断点信息。
或者通过vm.dispose直接中断相应的调试连接即可。

参考资料:

  • http://www.ibm.com/developerworks/cn/java/j-lo-jpda4/index.html
  • http://docs.oracle.com/javase/8/docs/jdk/api/jpda/jdi/index.html
  • http://elvis4139.iteye.com/blog/2225299


相关文章

发表评论

Comment form

(*) 表示必填项

还没有评论。

跳到底部
返回顶部