jvm调试工具arthas的watch命令记录函数参数和返回值案例

最近在一个项目在客户生产环境版本升级后,出现了预期之外的错误,由于客户升级的版本不是最新版本,是一个稍早一点的版本,时间已经是两年以前的了,对应的源码也很难找到和确认。另外,客户升级以前的版本对应功能也是正常的。

1. 在测试环境部署客户升级版本环境

为了查找问题原因,使用客户的升级版本,部署了环境。

2. 根据业务逻辑查找关键代码类和方法

虽然具体的版本不清楚,但是总体的业务处理流程应该是一致的,根据最新的代码和业务逻辑处理,找到关键的类和方法:

com.xx.yyy.business.handler.BaseHandlerImpl
getPwdSplit

3. 运行arthas,挂载到对应的java进程

这里需要注意的是,如果java进程不是root启动的,arthas需要和目标java进程使用相同的用户名启动,才能挂载成功。

4. 运行watch命令,观察入参和返回值

在arthas挂载成功的情况下,执行下面的命令:

[arthas@58938]$ watch com.xx.yyy.business.handler.BaseHandlerImpl getPwdSplit -b -s -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 13 , method count: 1) cost in 218 ms, listenerId: 3
method=com.xx.yyy.business.handler.BaseHandlerImpl.getPwdSplit location=AtEnter
ts=2024-03-10 11:41:53; [cost=0.032532ms] result=@ArrayList[
    @Object[][
        @ThirdAuthInfo[com.xx.yyy.business.entity.thirdauth.ThirdAuthInfo@29d4b0c],
        @String[1234test1234],
        @UserInfo[com.xx.yyy.business.entity.user.UserInfo@27bda07c],
        @ArrayList[isEmpty=true;size=0],
    ],
    @AuthService[
        log=@Logger[org.apache.log4j.Logger@e057bf3],
        enabled_jdbc=@Integer[0],
        log=@Logger[org.apache.log4j.Logger@3785f01],
        tokenFunc=null,
        log=@Logger[org.apache.log4j.Logger@69870874],
        raAttrType=@Integer[18],
        attrType=@Integer[12],
        retryCntType=@Integer[14],
        userServ=@UserServImpl[com.xx.yyy.business.service.user.impl.UserServImpl@23f3fb78],
        tokenServ=@TokenServImpl[com.xx.yyy.business.service.token.impl.TokenServImpl@68cd616],
        tokenExtServ=@TokenExtServImpl[com.xx.yyy.business.service.tokenext.impl.TokenExtServImpl@47cd0191],
        confServ=@ConfServImpl[com.xx.yyy.business.service.conf.impl.ConfServImpl@165875b4],
        agentServ=@AgentServImpl[com.xx.yyy.business.service.agent.impl.AgentServImpl@2a4c743c],
        userTokenServ=@UserTokenServImpl[com.xx.yyy.business.service.user_token.impl.UserTokenServImpl@4a103b05],
        pushSerServ=@PushSerServImpl[com.xx.yyy.business.service.pushserinfo.impl.PushSerServImpl@657e6a5f],
        assertionServ=@AssertionServImpl[com.xx.yyy.business.service.assertion.impl.AssertionServImpl@30de4691],
        enabled_jdbc=@Integer[0],
    ],
    null,
]
method=com.xx.yyy.business.handler.BaseHandlerImpl.getPwdSplit location=AtExit
ts=2024-03-10 11:41:53; [cost=2.378382086781499E9ms] result=@ArrayList[
    @Object[][
        @ThirdAuthInfo[com.xx.yyy.business.entity.thirdauth.ThirdAuthInfo@29d4b0c],
        @String[1234test1234],
        @UserInfo[com.xx.yyy.business.entity.user.UserInfo@27bda07c],
        @ArrayList[isEmpty=true;size=0],
    ],
    @AuthService[
        log=@Logger[org.apache.log4j.Logger@e057bf3],
        enabled_jdbc=@Integer[0],
        log=@Logger[org.apache.log4j.Logger@3785f01],
        tokenFunc=null,
        log=@Logger[org.apache.log4j.Logger@69870874],
        raAttrType=@Integer[18],
        attrType=@Integer[12],
        retryCntType=@Integer[14],
        userServ=@UserServImpl[com.xx.yyy.business.service.user.impl.UserServImpl@23f3fb78],
        tokenServ=@TokenServImpl[com.xx.yyy.business.service.token.impl.TokenServImpl@68cd616],
        tokenExtServ=@TokenExtServImpl[com.xx.yyy.business.service.tokenext.impl.TokenExtServImpl@47cd0191],
        confServ=@ConfServImpl[com.xx.yyy.business.service.conf.impl.ConfServImpl@165875b4],
        agentServ=@AgentServImpl[com.xx.yyy.business.service.agent.impl.AgentServImpl@2a4c743c],
        userTokenServ=@UserTokenServImpl[com.xx.yyy.business.service.user_token.impl.UserTokenServImpl@4a103b05],
        pushSerServ=@PushSerServImpl[com.xx.yyy.business.service.pushserinfo.impl.PushSerServImpl@657e6a5f],
        assertionServ=@AssertionServImpl[com.xx.yyy.business.service.assertion.impl.AssertionServImpl@30de4691],
        enabled_jdbc=@Integer[0],
    ],
    @String[][
        @String[1234test],
        @String[1234],
        @String[],
    ],
]

可以看出,当该方法被调用时,函数的入参和返回值都输出来了。这样对于确认这段代码的业务处理逻辑是否有错误就容易确认了。实际上这段业务的处理就是有问题的,返回值和预期的返回值不一样。

5. 反编译代码与正确的最新版本进行对比

通过jad命令,将对应方法的代码反编译出来,和最新版本的正确代码进行对比,因为总体逻辑基本上不会有大的变动,通过对比,很容易找出差异来。

[arthas@58938]$ jad com.xx.yyy.business.handler.BaseHandlerImpl getPwdSplit

通过代码对比,错误版本中,增加了一个多余的URL解码调用,导致%这样的字符当做转义字符处理了,而实际上,这里是不需要的。

6. watch命令参考

watch 的参数比较多,主要是因为它能在 4 个不同的场景观察对象

参数名称 参数说明
class-pattern 类名表达式匹配
method-pattern 函数名表达式匹配
express 观察表达式,默认值:{params, target, returnObj}
condition-express 条件表达式
[b] 函数调用之前观察
[e] 函数异常之后观察
[s] 函数返回之后观察
[f] 函数结束之后(正常返回和异常返回)观察
[E] 开启正则表达式匹配,默认为通配符匹配
[x:] 指定输出结果的属性遍历深度,默认为 1,最大值是 4
[m <arg>] 指定 Class 最大匹配数量,默认值为 50。长格式为[maxMatch <arg>]

这里重点要说明的是观察表达式,观察表达式的构成主要由 ognl 表达式组成,所以你可以这样写"{params,returnObj}",只要是一个合法的 ognl 表达式,都能被正常支持。

特别说明

  • watch 命令定义了 4 个观察事件点,即 -b 函数调用前,-e 函数异常后,-s 函数返回后,-f 函数结束后
  • 4 个观察事件点 -b-e-s 默认关闭,-f 默认打开,当指定观察点被打开后,在相应事件点会对观察表达式进行求值并输出
  • 这里要注意函数入参函数出参的区别,有可能在中间被修改导致前后不一致,除了 -b 事件点 params 代表函数入参外,其余事件都代表函数出参
  • 当使用 -b 时,由于观察事件点是在函数调用前,此时返回值或异常均不存在
  • 在 watch 命令的结果里,会打印出location信息。location有三种可能值:AtEnterAtExitAtExceptionExit。对应函数入口,函数正常 return,函数抛出异常。

更多内容还可以参考:

watch | arthas

相关推荐
撸码到无法自拔17 分钟前
深入理解.NET内存回收机制
jvm·.net
吴冰_hogan15 小时前
JVM(Java虚拟机)的组成部分详解
java·开发语言·jvm
东阳马生架构1 天前
JVM实战—1.Java代码的运行原理
jvm
ThisIsClark1 天前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
王佑辉1 天前
【jvm】内存泄漏与内存溢出的区别
jvm
大G哥1 天前
深入理解.NET内存回收机制
jvm·.net
泰勒今天不想展开1 天前
jvm接入prometheus监控
jvm·windows·prometheus
东阳马生架构2 天前
JVM简介—3.JVM的执行子系统
jvm
程序员志哥2 天前
JVM系列(十三) -常用调优工具介绍
jvm