转自云栖社区: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. 推荐20款JavaScript框架给前端开发者

    下面,我们给大家提供了一个用于 HTML5 开发的各种用途的 JavaScript 库列表.这些框架能够给前端开发人员提供更好的功能实现的解决方案.如果你有收藏优秀的框架,也可以在后面的评论中分享给我 ...

  2. Python3求英文文档中每个单词出现的次数并排序

    [本文出自天外归云的博客园] 题目要求: 1.统计英文文档中每个单词出现的次数. 2.统计结果先按次数降序排序,再按单词首字母降序排序. 3.需要考虑大文件的读取. 我的解法如下: import ch ...

  3. Python3回文相关算法小结

    [本文出自天外归云的博客园] 总结一下关于回文相关的算法: 判断字符串本身是否是回文 返回字符串中的所有子串 找到字符串中包含的所有回文 判断字符串中是否包含回文 将字符串变成一个不包含回文的字符串 ...

  4. python dataframe astype 字段类型转换

    使用dtype查看dataframe字段类型 print df.dtypes 使用astype实现dataframe字段类型转换 # -*- coding: UTF-8 -*- import pand ...

  5. Server.MapPath 出现未将对象引用设置到对象的实例

    此文仅在于使遇到相似问题的朋友能少走弯路 asp.net中我用一个页面的cs文件调用一个自定义类,这个自定义类再调用 System.Web.HttpContext.Current.Server.Map ...

  6. C++类默认函数

    问题,which is true??? 每个类都有一个无参构造函数 每个类都有一个拷贝构造函数 每个类可以有多个构造函数 每个类可以多个析构函数 默认构造函数   析构函数   拷贝构造函数   赋值 ...

  7. SnowNLP:•中文分词•词性标准•提取文本摘要,•提取文本关键词,•转换成拼音•繁体转简体的 处理中文文本的Python3 类库

    SnowNLP是一个python写的类库,可以方便的处理中文文本内容,是受到了TextBlob的启发而写的,由于现在大部分的自然语言处理库基本都是针对英文的,于是写了一个方便处理中文的类库,并且和Te ...

  8. VC/Wince 实现仿Win8 Metro风格界面2——页面滑动切换(附效果图)

    前几天开始写仿Win8 Metro界面文章,部分网友觉得不错,感谢各位的意见.本来今天一直在折腾Android VLC播放器,没时间写.不过明天休息,所以今天就抽时间先写一下. 言归正传,我们都知道W ...

  9. Mac OS X下的移动光标和文字编辑快捷键

    移动光标快捷键 Control-F 光标前进一个字符,相当于右键(F = Forward) Control-B 光标后退一个字符,相当于左键(B = Backward) Control-P 上移一行, ...

  10. android 中使用jwt token(json web token)--java

    http://blog.csdn.net/mingzhnglei/article/details/51119836 下面贴上自己项目中的一个小小的example import com.nimbusds ...