转自云栖社区:https://yq.aliyun.com/articles/56?spm=5176.100239.blogcont59193.11.jOh3ZG#

摘要: 该文章来自于阿里巴巴技术协会(ATA)精选文章。 Java调试概述 程序猿都调式或者debug过Java代码吧?都体会过被PM,PD,测试,业务同学们围观debug吧?说调试,先看看调试严格定义是什么。引用Wikipedia定义: 调试(De-bug),又称除错,是发现和减少计

该文章来自于阿里巴巴技术协会(ATA)精选文章。

Java调试概述

程序猿都调式或者debug过Java代码吧?都体会过被PM,PD,测试,业务同学们围观debug吧?说调试,先看看调试严格定义是什么。引用Wikipedia定义

调试(De-bug),又称除错,是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。调试的基本步骤:
1. 发现程序错误的存在
2. 以隔离、消除的方式对错误进行定位
3. 确定错误产生的原因
4. 提出纠正错误的解决办法
5. 对程序错误予以改正,重新测试

用调试的好处是我们就无需每次新测试都要重新编译了,不用copy-paste一堆的System.out.println(很low但很多时候很管用有没有?)。

更多时候我们调试最直接简单的办法就是IDE,Java程序员用的最多的必然是Eclipse,Netbeans和IntelliJ也有各自忠实的粉丝,各有优劣。关于用IDE如何调试可以另起一个话题再讨论。

除了IDE之外,JDK也自带了一些命令行调试工具也很方便。大家用的比较多的如下表所示:

命令 描述
jdb 命令行调试工具
jps 列出所有Java进程的PID
jstack 列出虚拟机进程的所有线程运行状态
jmap 列出堆内存上的对象状态
jstat 记录虚拟机运行的状态,监控性能
jconsole 虚拟机性能/状态检查可视化工具

具体用法可以参考JDK文档,这些大家在线上调试应用的时候用的也不少,比如一般线上load高的问题排查步骤是

  1. 先用top找到耗资源的进程
  2. ps+grep找到对应的java进程/线程
  3. jstack分析哪些线程阻塞了,阻塞在哪里
  4. jstat看看FullGC频率
  5. jmap看看有没有内存泄露

但这个也不是今天的重点,那么问题来了(blue fly is the strongest):这些工具如何能获取远程Java进程的信息的?又是如何远程控制Java进程的运行的? 相信有不少人和我一样对这些工具的 实现原理 很好奇,本文就尝试介绍下各中缘由。

Java调试体系JPDA简介

Java虚拟机设计了专门的API接口供调试和监控虚拟机使用,被称为Java平台调试体系即Java Platform Debugger Architecture(JPDA)。JPDA按照抽象层次,又分为三层,分别是

  • JVM TI - Java VM Tool Interface

    • 虚拟机对外暴露的接口,包括debug和profile
  • JDWP - Java Debug Wire Protocol
    • 调试器和应用之间通信的协议
  • JDI - Java Debug Interface
    • Java库接口,实现了JDWP协议的客户端,调试器可以用来和远程被调试应用通信

用一个不是特别准确但是比较容易理解的类比,大家可以和HTTP做比较,可以推断他就是一个典型的C/S应用,所以也可以很自然的想到,JDI是用TCP Socket和虚拟机通信的,后面会详细再介绍。

  • IDE+JDI = 浏览器
  • JDWP = HTTP
  • JVMTI = RESTful接口
  • Debugee虚拟机= REST服务端

和其他的Java模块一样,Java只定义了Spec规范,也提供了参考实现(Reference Implementation),但是第三方完全可以参照这个规范,按照自己的需要去实现其中任意一个组件,原则上除了规范上没有定义的功能,他们应该能正常的交互,比如Eclipse就没有用Sun/Oracle的JDI,而是自己实现了一套(由于开源license的兼容原因),因为直接用JDWP协议调用JVMTI是不会受GPL“污染”的。的确有第三方调试工具基于JVMTI做了一套调试工具,这样效率更高,功能更丰富,因为JDI出于远程调用的安全考虑,做了一些功能的限制。用户还可以不用JDI,用自己熟悉的C或者脚本语言开发客户端,远程调试Java虚拟机,所以JPDA真个架构是非常灵活的。

JVMTI

JVMTI是整个JPDA中最中要的API,也是虚拟机对外暴露的接口,掌握了JVMTI,你就可以真正完全掌控你的虚拟机,因为必须通过本地加载,所以暴露的丰富功能在安全上也没有太大问题。更完整的API内容可以参考JVMTI SPEC:

  • 虚拟机信息

    • 堆上的对象
    • 线程和栈信息
    • 所有的类信息
    • 系统属性,运行状态
  • 调试行为
    • 设置断点
    • 挂起现场
    • 调用方法
  • 事件通知
    • 断点发生
    • 异步调用

在JPDA的这个图里,agent是其中很重要的一个模块,正是他把JDI,JDWP,JVMTI三部分串联成了一个整体。简单来说agent的特性有

  • C/C++实现的
  • 被虚拟机以动态库的方式加载
  • 能调用本地JVMTI提供的调试能力
  • 实现JDWP协议服务器端
  • 与JDI(作为客户端)通信(socket/shmem等方式)

Code speak louder than words. 上个代码加注释来解释:

// Agent_OnLoad必须是入口函数,类似于main函数,规范规定
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{
....
MethodTraceAgent* agent = new MethodTraceAgent();
agent->Init(vm);
agent->AddCapability();
agent->RegisterEvent();
...
} /****** AddCapability(): init(): 初始化jvmti函数指针,所有功能的函数入口 *****/
jvmtiEnv* MethodTraceAgent::m_jvmti = 0;
jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0); /****** AddCability(): 确认agent能访问的虚拟机接口 *****/
jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_generate_method_entry_events = 1;
// 设置当前环境
m_jvmti->AddCapabilities(&caps); /****** RegisterEvent(): 创建一个新的回调函数 *****/
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;
// 设置回调函数
m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
// 开启事件监听
m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0); /****** HandleMethodEntry: 注册的回调,获取对应的信息 *****/
// 获得方法对应的类
m_jvmti->GetMethodDeclaringClass(method, &clazz);
// 获得类的签名
m_jvmti->GetClassSignature(clazz, &signature, 0);
// 获得方法名字
m_jvmti->GetMethodName(method, &name, NULL, NULL);

写好agent后,需要编译,并在启动Java进程时指定加载路径

// 编译动态链接库
g++ -w -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libAgent.so // 拷贝到 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/home/xiaoxia/lib
cp libAgent.so ~/lib // 运行测试效果,记得load编译的动态库
javac MethodTraceTest.java
java -agentlib:Agent=first MethodTraceTest

Agent实现的动态链接库其实有两种加载方式:

  • 虚拟机启动初期加载 这个链接库必须实现Agent_OnLoad作为函数入口。这种方式可以利用的接口和功能更多,因为他在被调式虚拟机运行的应用初始化之前就被调用了,但是限制是必须以显示的参数指定启动方式,这在线上环境上是不大现实的。
 java -agentlib:<agent-lib-name>=<options> JavaClass
//Linux从LD_LIBRARY_PATH找so文件, Windows从PATH找该DLL文件。
java -agentpath:<path-to-agent>=<options> JavaClass
//直接从绝对路径查找
  • 动态加载 这是更灵活的方式,Java进程可以正常启动,如果需要,通过Sun/Orale提供的私有Attach API可以连上对应的虚拟机,再通过JPDA方式控制,不过因为虚拟机已经开始运行了,所以功能上会有限制。我们比较熟悉的jstack等jdk工具就是通过这种方式做的,动态库必须实现Agent_OnAttach作为函数入口。如果有兴趣理解Attach机制细节的话,可以参考这个blog,简单来说,就是虚拟机默认起了一个线程(没错,就是jstack时看到Signal Dispatcher这货),专门接受处理进程间singal通知,当他收到SIGQUIT时,就会启动一个新的socket监听线程(就是jstack看到的Attach Listener线程)来接收命令,Attach Listener就是一个agent实现,他能处理很多dump命令,更重要的是他能再加载其他agent,比如jdwp agent。

通过Attach机制,我们能自己非常方便的实现一个jinfo或者其他jdk tools,只需通过JPS获取pid,在通过attach api去load我们提供的agent,完整的jinfo例子也在附件里。

import java.io.IOException;
import com.sun.tools.attach.VirtualMachine; public class JInfo { public static void main(String[] args) throws Exception {
String pid = args[0];
String agentName = "JInfoAgent"; System.out.printf("Atach to Pid %s, dynamic load agent %s \n", pid, agentName);
VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);
virtualMachine.loadAgentLibrary(agentName, null);
virtualMachine.detach();
}
}

JDWP

JDWP 是 Java Debug Wire Protocol 的缩写,它定义了调试器(debugger)和被调试的 Java 虚拟机(debugee)之间的通信协议。他就是同过JVMTI Agent实现的,简单来说,他就是对JVMTI调用(输入和输出,事件)的通信定义。

JDWP 有两种基本的包(packet)类型:命令包(command packet)和回复包(reply packet)。JDWP 本身是无状态的,因此对 命令出现的顺序并不受限制。而且,JDWP 可以是异步的,所以命令的发送方不需要等待接收到回复就可以继续发送下一个命令。Debugger 和 Debugee 虚拟机都有可能发送命令:

  • Debugger 通过发送命令获取Debugee虚拟机的信息以及控制程序的执行。Debugger虚拟机通过发送 命令通知 Debugger 某些事件的发生,如到达断点或是产生异常。
  • 回复是用来确认对应的命令是否执行成功(在包定义有一个flag字段对应),如果成功,回复还有可能包含命令请求的数据,比如当前的线程信息或者变量的值。从 Debugee虚拟机发送的事件消息是不需要回复的。

下图展示了一个可能的实现方式,再次强调下,Java的世界里只定义了规范(Spec),很多实现细节可以自己提供,比如虚拟机就有很多中实现(Sun HotSpot,IBM J9,Google Davik)。

一般我们启动远程调试时,都会看到如下参数,其实表面了JDWP Agent就是通过启动一个socket监听来接受JDWP命令和发送事件信息的,而且,这个TCP连接可以是双向的:

// debugge是server先启动监听,ide是client发起连接
agentlib:jdwp=transport=dt_socket,server=y,address=8000 // debugger ide是server,通过JDI监听,JDWP Agent作为客户端发起连接
agentlib:jdwp=transport=dt_socket,address=myhost:8000

JDI

JDI属于JPDA中最上层接口,也是Java程序员接触的比较多的。他用起来也比较简单,参考JDI的API Doc即可。所有的功能都和JVMTI提供的调试功能一一对应的(JVMTI还包括很多非调式接口,JDK5以前JVMTI是分为JVMDI和JVMPI的,分别对应调试debug和调优profile)。

还是用一个例子来解释最直接,大家可以看到基本的流程都是类似的,真个JPDA调试的核心就是通过JVMTI的 调用 和事件 两个方向的沟通实现的。

import java.util.List;
import java.util.Map;
import com.sun.jdi.*;
import com.sun.jdi.connect.*;
import com.sun.jdi.event.*;
import com.sun.jdi.request.*; public class MethodTrace {
private VirtualMachine vm;
private Process process;
private EventRequestManager eventRequestManager;
private EventQueue eventQueue;
private EventSet eventSet;
private boolean vmExit = false;
//write your own testclass
private String className = "MethodTraceTest"; public static void main(String[] args) throws Exception { MethodTrace trace = new MethodTrace();
trace.launchDebugee();
trace.registerEvent(); trace.processDebuggeeVM(); // Enter event loop
trace.eventLoop(); trace.destroyDebuggeeVM(); } public void launchDebugee() {
LaunchingConnector launchingConnector = Bootstrap
.virtualMachineManager().defaultConnector(); // Get arguments of the launching connector
Map<String, Connector.Argument> defaultArguments = launchingConnector
.defaultArguments();
Connector.Argument mainArg = defaultArguments.get("main");
Connector.Argument suspendArg = defaultArguments.get("suspend"); // Set class of main method
mainArg.setValue(className);
suspendArg.setValue("true");
try {
vm = launchingConnector.launch(defaultArguments);
} catch (Exception e) {
// ignore
}
} public void processDebuggeeVM() {
process = vm.process();
} public void destroyDebuggeeVM() {
process.destroy();
} public void registerEvent() {
// Register ClassPrepareRequest
eventRequestManager = vm.eventRequestManager();
MethodEntryRequest entryReq = eventRequestManager.createMethodEntryRequest(); entryReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
entryReq.addClassFilter(className);
entryReq.enable(); MethodExitRequest exitReq = eventRequestManager.createMethodExitRequest();
exitReq.addClassFilter(className);
exitReq.setSuspendPolicy(EventRequest.SUSPEND_EVENT_THREAD);
exitReq.enable();
} private void eventLoop() throws Exception {
eventQueue = vm.eventQueue();
while (true) {
if (vmExit == true) {
break;
}
eventSet = eventQueue.remove();
EventIterator eventIterator = eventSet.eventIterator();
while (eventIterator.hasNext()) {
Event event = (Event) eventIterator.next();
execute(event);
if (!vmExit) {
eventSet.resume();
}
}
}
} private void execute(Event event) throws Exception {
if (event instanceof VMStartEvent) {
System.out.println("VM started");
} else if (event instanceof MethodEntryEvent) {
Method method = ((MethodEntryEvent) event).method();
System.out.printf("Enter -> Method: %s, Signature:%s\n",method.name(),method.signature());
System.out.printf("\t ReturnType:%s\n", method.returnTypeName());
} else if (event instanceof MethodExitEvent) {
Method method = ((MethodExitEvent) event).method();
System.out.printf("Exit -> method: %s\n",method.name());
} else if (event instanceof VMDisconnectEvent) {
vmExit = true;
}
}
}

总结

整个JDPA有非常清晰的分层,各司其职,让整个调式过程简单可以扩展,而这一切其实都是构建在高司令巨牛逼的Java虚拟机抽象之上的,通过JVMTI将抽象良好的虚拟机控制暴露出来,让开发者可以自由的掌控被调试的虚拟机。有兴趣的同学可以运行下附近中的几个例子,应该会有更充分的了解。

而且由于规范的灵活性,如果有特殊需求,完全可以自己去重新实现和扩展,而且不限于Java,举个例子,我们可以通过agent去加密解密加载的类,保护知识产权;我们可以记录虚拟机运行过程,作为自动化测试用例; 我们还可以把线上问题的诊断实践自动化下来,做一个快速预判 ,争取最宝贵的时间。

参考文档

 
 
本文为云栖社区原创内容,未经允许不得转载,如需转载请发送邮件至yqeditor@list.alibaba-inc.com;如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:yqgroup@service.aliyun.com 进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。

Java调试那点事[转]的更多相关文章

  1. Java日志性能那些事(转)

    在任何系统中,日志都是非常重要的组成部分,它是反映系统运行情况的重要依据,也是排查问题时的必要线索.绝大多数人都认可日志的重要性,但是又有多少人仔细想过该怎么打日志,日志对性能的影响究竟有多大呢?今天 ...

  2. Java调试

    线上load高的问题排查步骤是: 先用top找到耗资源的进程 ps+grep找到对应的java进程/线程 jstack分析哪些线程阻塞了,阻塞在哪里 jstat看看FullGC频率 jmap看看有没有 ...

  3. java调试一

    Eclipse 平台的特色在于内置了 Java 调试器,该调试器提供所有标准调试功能,包括进行单步执行.设置断点和值.检查变量和值以及暂挂和恢复线程的能力.Eclipse 平台工作台(Eclipse ...

  4. 深入 Java 调试体系: 第 1 部分,初探JPDA 体系

    JPDA(Java Platform Debugger Architecture)是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程 ...

  5. java调试器

    javac.exe是编译.java文件 java.exe是执行编译好的.class文件 javadoc.exe是生成Java说明文档 jdb.exe是Java调试器 javaprof.exe是剖析工具 ...

  6. 关于代码调试de那些事

    原文出处:http://www.wklken.me/posts/2014/11/23/how-to-debug.html 关于代码调试de那些事 1.你得明白你在做什么, 保持清醒 2.想清楚了再写代 ...

  7. Java调试平台体系JPDA

    Java 平台调试体系(Java Platform Debugger Architecture,JPDA)定义了一个完整独立的体系,它由三个相对独立的层次共同组成,而且规定了它们三者之间的交互方式,或 ...

  8. 深入 Java 调试体系: 第 1 部分,JPDA 体系概览

    JPDA 概述 所有的程序员都会遇到 bug,对于运行态的错误,我们往往需要一些方法来观察和测试运行态中的环境.在 Java 程序中,最简单的,您是否尝试过使用 System.out.println( ...

  9. 两万字长文总结,梳理 Java 入门进阶那些事

    大家好,我是程序员小跃,一名在职场已经写了6年程序的老程序员,从一开始的菊厂 Android 开发到现在某游戏公司的Java后端架构,对Java还是相对了解的挺多. 大概是半年前吧,在知乎上有个知友私 ...

随机推荐

  1. 每日英语:Boost Your Balance; Avoid Falls

    If you find yourself needing to sit down to take off your shoes, it might be time to start paying at ...

  2. RIFF格式简介

    Resource Interchange File Format(简称RIFF),资源交换文件格式,是一种按照标记区块存储数据(tagged chunks)的通用文件存储格式,多用于存储音频.视频等多 ...

  3. 【密码学】RSA公钥密码体制

    RSA公钥密码体制是美国麻省理工学院(MIT)的三位科学家Rivest.Shamir.Adleman于1978年提出的,简称RSA公钥秘密系统.实际上,RSA稍后于MH背包公钥密码实用系统,但它的影响 ...

  4. 【C++程序员学 python】python split and join 分割与合并

    感觉这名字有点不对,但不知道用什么好,就将就吧. 坑爹啊,居然要把符号放在前面.

  5. C#学习笔记(14)——C# 使用IComparer自定义List类的排序方案

    说明(2017-7-17 21:34:59): 原文:https://my.oschina.net/Tsybius2014/blog/298702?p=1 另一篇比较好的:https://wenku. ...

  6. stdafx

    Standard Application Fram Extend没有函数库,只是定义了一些环境参数,使得编译出来的程序能在32位的操作系统环境下运行. Windows和MFC的include文件都非常 ...

  7. 开源一个爬取redmine数据的测试报告系统

    背景 软件测试的最后有一道比较繁琐的工作,就是编写测试报告.手写测试报告在数据统计和分析上面要耗费比较大的事件和精力.之前工作室使用mantis管理bug缺陷.公司有内部有个系统,可以直接从manti ...

  8. mac上校验文件的 md5 sha-1

    文件校验 mac md5 sha-1html, body {overflow-x: initial !important;}.CodeMirror { height: auto; } .CodeMir ...

  9. Android——indexof()

    Java中字符串中子串的查找共有四种方法(indexof()) Java中字符串中子串的查找共有四种方法,如下:1.int indexOf(String str) :返回第一次出现的指定子字符串在此字 ...

  10. [转]TF-IDF与余弦相似性的应用(一):自动提取关键词

    这个标题看上去好像很复杂,其实我要谈的是一个很简单的问题. 有一篇很长的文章,我要用计算机提取它的关键词(Automatic Keyphrase extraction),完全不加以人工干预,请问怎样才 ...