Java调试那点事[转]

转自云栖社区: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去加密解密加载的类,保护知识产权;我们可以记录虚拟机运行过程,作为自动化测试用例; 我们还可以把线上问题的诊断实践自动化下来,做一个快速预判 ,争取最宝贵的时间。

参考文档

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

时间: 2024-10-15 20:33:26

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

Java调试那点事

转自: http://yq.aliyun.com/articles/56?hmsr=toutiao.io&spm=5176.100240.searchblog.18&utm_medium=toutiao.io&utm_source=toutiao.io Java调试那点事 该文章来自于阿里巴巴技术协会(ATA)精选文章. Java调试概述 程序猿都调式或者debug过Java代码吧?都体会过被PM,PD,测试,业务同学们围观debug吧?说调试,先看看调试严格定义是什么.引用Wik

java调试器

javac.exe是编译.java文件 java.exe是执行编译好的.class文件 javadoc.exe是生成Java说明文档 jdb.exe是Java调试器 javaprof.exe是剖析工具 一.区别 (A)java.exe:              运行java程序 javaw.exe:              跟java命令相对的,运行java命令时,会出现并保持一个console窗口,        程序中的信息可以通过System.out在console内输出,而运行jav

JAVA调试

深入 Java 调试体系: 第 1 部分,JPDA 体系概览 深入 Java 调试体系,第 2 部分: JVMTI 和 Agent 实现 深入 Java 调试体系,第 3 部分: JDWP 协议及实现 深入 Java 调试体系,第 4             部分: Java 调试接口(JDI) https://yq.aliyun.com/articles/56?spm=5176.8067842.tagmain.74.RLrk6z http://docs.oracle.com/javase/8/

关于代码调试de那些事

原文出处:http://www.wklken.me/posts/2014/11/23/how-to-debug.html 关于代码调试de那些事 1.你得明白你在做什么, 保持清醒 2.想清楚了再写代码 3.关于脚手架代码 4.写完一段代码第一时间自己review一下 5.review中注意, 代码是抠过来的么? 6.搞明白问题的表现是什么(症状) 7.调试过程中, 需要时刻注意 8.环境/数据一致性 9.先不要动代码, 假设代码是正确的 10.首先要怀疑自己 11.对于莫名其妙的问题, 多试几

java序列化Serializable那些事

串行化也叫序列化,就是将实例的状态转化成文本(或二近制)的形式,以便永久保存(所以有时候也叫持久化,或者信息的冷藏等等)或在网间传递.也就是说,如果一个类的实例需要持久化或者需要在网间传递的时候,就用到了串行化  Serialization是指把类或者基本的数据类型持久化(persistence)到数据流(Stream)中,包括文件.字节流.网络数据流.         JAVA中实现serialization主要靠两个类:ObjectOuputStream和ObjectInputStream.

关于JAVA多线程的那些事__初心者

前言 其实事情的经过也许会复杂了点,这事还得从两个月前开始说.那天,我果断不干IT支援.那天,我立志要做一个真正的程序猿.那天,我26岁11个月.那天,我开始看Android.那天,我一边叨念着有朋自远方来,一边投身了JAVA的怀抱.那天,一切将会改变. 好吧,反正总的来说就是时隔4年半,我又开始搞JAVA了.Eclipse还是Eclipse:NetBeans还是NetBeans:Java被收之后已经来到了7,现在是8:在入手了几本JAVA的书籍后发现<JAVA编程思想>还是这么伟大:开始了新

聊聊高并发(三十六)Java内存模型那些事(四)理解Happens-before规则

在前几篇将Java内存模型的那些事基本上把这个域底层的概念都解释清楚了,聊聊高并发(三十五)Java内存模型那些事(三)理解内存屏障 这篇分析了在X86平台下,volatile,synchronized, CAS操作都是基于Lock前缀的汇编指令来实现的,关于Lock指令有两个要点: 1. lock会锁总线,总线是互斥的,所以lock后面的写操作会写入缓存和内存,可以理解为在lock后面的写缓存和写内存这两个动作称为了一个原子操作.当总线被锁时,其他的CPU是无法使用总线的,也就让其他的读写都等

Java调试小例子(如何使用F5和F6)

如果一个类有3个方法,就在3个方法的前面设置断点,总共就3个断点,然后进入Debug调试, 一直按F6(Step Over)他会在方法那里直接进入方法而不需要你按F5(Step  Into) 或者你只是在第一个方法那里设置了一个断点,那么在调试时要进入方法就需要按F5(Step Into)来进入方法. package args参数问题; /* * 思考题1:看程序写结果,然后分析为什么是这个样子的.并画图讲解.最后总结Java中参数传递规律. * Java中的参数传递问题: 基本类型:形式参数的

java 调试

作为一名java开发程序员,或者有时候需要利用工具调试的时候,但是却感觉不会使用,其实只要记住四个键即可. 一般java开发工具使用的都是Eclipse或者MyEclipse,下面都有这几个键F5(进入方法),F6(单步向下执行),F7(退出该方法),F8(结束调试,如果有下一个断点即跳入到下一个断点位置处) 一般调试如下: 1,对需要调试的地方打断点,点击左边行数 2,进入Debug模式,点击视图的Debug 3,F5是单步进入方法 4,F6是单步往下执行 5,F7是退出当前方法 6,F8是结