该文章来自于阿里巴巴技术协会(ATA)精选文章。java
Java调试概述
程序猿都调式或者debug过Java代码吧?都体会过被PM,PD,测试,业务同窗们围观debug吧?说调试,先看看调试严格定义是什么。引用Wikipedia定义:linux
调试(De-bug),又称除错,是发现和减小计算机程序或电子仪器设备中程序错误的一个过程。调试的基本步骤:
1. 发现程序错误的存在
2. 以隔离、消除的方式对错误进行定位
3. 肯定错误产生的缘由
4. 提出纠正错误的解决办法
5. 对程序错误予以改正,从新测试程序员
用调试的好处是咱们就无需每次新测试都要从新编译了,不用copy-paste一堆的System.out.println(很low但不少时候很管用有没有?)。apache
更多时候咱们调试最直接简单的办法就是IDE,Java程序员用的最多的必然是Eclipse,Netbeans和IntelliJ也有各自忠实的粉丝,各有优劣。关于用IDE如何调试能够另起一个话题再讨论。
api
除了IDE以外,JDK也自带了一些命令行调试工具也很方便。你们用的比较多的以下表所示:浏览器
命令 |
描述 |
jdb |
命令行调试工具 |
jps |
列出全部Java进程的PID |
jstack |
列出虚拟机进程的全部线程运行状态 |
jmap |
列出堆内存上的对象状态 |
jstat |
记录虚拟机运行的状态,监控性能 |
jconsole |
虚拟机性能/状态检查可视化工具 |
具体用法能够参考JDK文档,这些你们在线上调试应用的时候用的也很多,好比通常线上load高的问题排查步骤是安全
- 先用top找到耗资源的进程
- ps+grep找到对应的java进程/线程
- jstack分析哪些线程阻塞了,阻塞在哪里
- jstat看看FullGC频率
- jmap看看有没有内存泄露
但这个也不是今天的重点,那么问题来了(blue fly is the strongest):这些工具如何能获取远程Java进程的信息的?又是如何远程控制Java进程的运行的? 相信有很多人和我同样对这些工具的 实现原理 很好奇,本文就尝试介绍下各中原因。bash
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去加密解密加载的类,保护知识产权;咱们能够记录虚拟机运行过程,做为自动化测试用例; 咱们还能够把线上问题的诊断实践自动化下来,作一个快速预判 ,争取最宝贵的时间。
参考文档