Arthas之vmtool:如何查看线上JVM中的java实例对象?
点击查看vmtool官方教程
vmtool介绍
最近短信系统的客户反馈短信报告缺失。运营商推送了所有的短信报告给我们,但是我们系统却只推送了部分报告给客户,中间丢失了部分报告,程序反复检查了了很多遍也没有发现问题。经过我们排查之后推测可能是该短信通道创建了多个实例,并且每个实例的参数配置不同导致出现异常。为了验证这个推测,我们决定使用 Arthas 中的 vmtool 工具进行深入排查 ,检测线上JVM系统中的通道实例数量以及参数配置。
vmtool 功能介绍
在 Arthas 这个强大的工具库中,vmtool 工具主要利用 JVMTI(Java Virtual Machine Tool Interface)接口来实现一系列强大的功能。JVMTI 接口为开发者提供了一种深入 JVM 内部的能力,能够对 JVM 的运行时状态进行监控和操作。vmtool 借助这个接口,可以实现查询内存对象,在复杂的内存空间里获取我们关心的对象信息 。在排查我们线上短信系统的问题时,就需要借助 vmtool 来查询短信通道相关的实例对象,了解它们的属性和状态。
同时,vmtool 还具备强制 GC(Garbage Collection,垃圾回收)的功能。在 Java 应用运行过程中,垃圾回收机制负责回收不再使用的内存空间,以保证应用的内存使用效率。但有时候,我们可能需要手动触发垃圾回收来观察内存的变化情况,或者在某些特定场景下,希望立即回收一些不再使用的对象所占用的内存,这时 vmtool 的强制 GC 功能就派上用场了。不过在我们这次解决短信系统问题的过程中,主要用到的还是它查询内存对象的功能,通过它来揭开通道实例数量异常以及实例属性未生效背后的真相。
vmtool基本语法与参数
本次使用的arthas版本是4.0.5,不同版本的参数和用法可能都不同,所以在使用时可以先使用
vmtool -h
命令查看帮助文档。帮助文档中有当前命令支持的参数以及使用示例。
vmtool -a {forceGc, getInstances, interruptThread}
[-c <value>] [--classLoaderClass <value>] [--className <value>]
[-x <value>] [--express <value>]
[-h] [--libPath <value>]
[-l <value>]
[-t <value>]
常用参数及其作用如下:
--action:指定要执行的操作,是 vmtool 命令的核心参数,它决定了 vmtool 的具体行为。比如
getInstances
用于获取指定类的实例;forceGc
用于强制执行垃圾回收;interruptThread
用于中断线程。
--className:指定要操作的类的全限定名,精确地告诉 vmtool 我们关注的是哪个类。例如java.lang.String,让 vmtool 针对String类进行操作。
--classLoaderClass:指定类加载器的类名,当需要从特定的类加载器中获取信息时使用。在复杂的类加载环境中,不同的类加载器可能加载了相同类的不同版本。
--express:指定 OGNL(Object - Graph Navigation Language)表达式,用于对获取到的对象进行进一步的处理和查询。OGNL 表达式就像一种特殊的查询语言,能够深入对象内部,获取其属性值或调用其方法。例如instances[0].fieldName可以获取到第一个实例的fieldName属性值。
--limit:限制返回的实例数量,在获取大量对象时非常有用,可以避免因返回过多数据而导致 JVM 性能下降。
-x/--expand:指定结果的展开层次,默认值是 1。对于复杂的对象,通过这个参数可以控制输出的详细程度。例如,当查询一个包含多层嵌套对象的实例时,-x 3可以让我们看到更深入层次的对象属性。
vmtool常用命令与示例
查找类的所有实例
# 该命令表示 我们需要查询 --className 指定的类的实例, --limit 3 参数限制了返回的实例数量为 3 个,这在处理大量对象时可以有效避免对 JVM 性能造成过大压力。如果不指定--limit参数,默认会返回 10 个实例。 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --limit 3 @CommonSmppChannel[][ @CmppChannel[com.cly.sms.gateway.tcp.protocol.cmpp.CmppChannel@5531577b], @CmppChannel[com.cly.sms.gateway.tcp.protocol.cmpp.CmppChannel@35f0235a], @CmppChannel[com.cly.sms.gateway.tcp.protocol.cmpp.CmppChannel@36a03a40] ]
输出结果会包含每个实例的
hashCode
、类名和简要信息(如字符串值)。也可以添加 -x 参数控制返回的实例的展开层次,例如:
[arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --limit 1 -x 2 @CommonSmppChannel[][ @CmppChannel[ dbLoader=@DbLoader[com.cly.sms.gateway.common.loader.DbLoader@55c3f019], rocketMqLoader=@RocketMqLoader[com.cly.sms.gateway.common.loader.RocketMqLoader@63d52f3f], gatewaySendConsumer=@GatewaySendConsumer[com.cly.sms.gateway.common.core.GatewaySendConsumer@348df198], ], ]
输出结果显示,该实例是CommonSmppChannel 类的子类CmppChannel的实例对象,并且该对象中有dbLoader、rocketMqLoader、gatewaySendConsumer三个属性。
查看对象属性
-x能展开查看所有的属性,但是属性太多或者层级太深查看起来就非常困难。我们可以通过--express参数结合 OGNL 表达式来过滤实例或者查看指定属性。
过滤实例的命令如下:
# 这里增加了 --express 表达式来过滤实例。 # getInstances action 返回结果绑定到instances变量上,它是数组。 # 我们可以使用iterator遍历该数组,其中 ?是表示过滤 符合后面条件的记录 # 更多使用方法 可以搜索 ognl投影表达式用法 # 此命令的就是查询CommonSmppChannel类的实例,并且实例的gatewaySendConsumer.channel.channelParam.channelNo属性值是300000139 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{? #this.gatewaySendConsumer.channel.channelParam.channelNo==300000139}' @ArrayList[ @CmppChannel[ dbLoader=@DbLoader[com.cly.sms.gateway.common.loader.DbLoader@625fad04], rocketMqLoader=@RocketMqLoader[com.cly.sms.gateway.common.loader.RocketMqLoader@db0846a], gatewaySendConsumer=@GatewaySendConsumer[com.cly.sms.gateway.common.core.GatewaySendConsumer@73935a97], ], ]
此次输出的实例就是channelNo=300000139的实例,可以过滤掉其他多余的实例。我们可以再修改表达式,只展示指定的属性。
# 在该命令中增加了 #this.gatewaySendConsumer.channel.channelParam 对已经过滤出的实例的channelParam属性进行了打印,防止其他多余的属性影响我们排查故障。 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{? #this.gatewaySendConsumer.channel.channelParam.channelNo==300000139}.{#this.gatewaySendConsumer.channel.channelParam}' -x 2 @ArrayList[ @ChannelParam[ serialVersionUID=@Long[1], id=@Integer[785], channelNo=@Integer[300000139], account=@String[300000], password=@String[300000], sourceId=@String[106901], protocol=@String[standardCMPP], runGatewayNo=null, note=@String[], extParamMap=@HashMap[isEmpty=false;size=3], ], ]
--express 表达式还有很多用法,可以参考Arthas OGNL表达式使用
调用对象方法
# 在第一步过滤了channelNo=300000139的实例之后,可以调用该实例中的setAccount()方法,比如修改账号信息。但是如果第一步匹配到了多个实例,将会修改所有实例的账号。 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{? #this.gatewaySendConsumer.channel.channelParam.channelNo==300000139}.{#this.gatewaySendConsumer.channel.channelParam.setAccount("200xxxx")}' @ArrayList[ null, ] # 调用getAccount方法,账号信息已经修改成功 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{? #this.gatewaySendConsumer.channel.channelParam.channelNo==300000139}.{#this.gatewaySendConsumer.channel.channelParam.getAccount()}' @ArrayList[ @String[200xxxx], ] # 如果过滤后仍然匹配到多个实例,每个实例的getAccount方法都会执行 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{? #this.gatewaySendConsumer.channel.channelParam.account=="200"}.{#this.gatewaySendConsumer.channel.channelParam.getAccount()}' @ArrayList[ @String[200], @String[200], ] # 可以 get(0),取第一个实例 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{? #this.gatewaySendConsumer.channel.channelParam.account=="200"}.get(0).{#this.gatewaySendConsumer.channel.channelParam.getAccount()}' @ArrayList[ @String[200], ] # 也可以直接取CommonSmppChannel类的第一个实例,然后调用getAccount()方法 [arthas@90648]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express 'instances[0].gatewaySendConsumer.channel.channelParam.getAccount()' @String[200]
强制垃圾回收
在 JVM 的运行过程中,垃圾回收(GC)是一个重要的机制,它负责回收不再被使用的对象所占用的内存空间,以确保 JVM 有足够的内存来运行应用程序。然而,在某些特殊情况下,我们可能需要手动触发垃圾回收,这时就可以使用 vmtool 命令的--action forceGc参数来实现。
当我们怀疑应用程序存在内存泄漏问题,或者想要在特定时刻释放内存以观察应用程序的性能变化时,手动触发垃圾回收是非常有用的。例如,在进行性能测试时,我们可以在测试前后分别执行垃圾回收操作,以确保测试环境的一致性,减少内存因素对测试结果的影响。
使用 vmtool 命令强制触发垃圾回收非常简单,只需要执行以下命令:
vmtool --action forceGc
执行该命令后,JVM 会立即启动垃圾回收机制,对堆内存中的对象进行扫描和回收。在垃圾回收过程中,JVM 会检查哪些对象不再被引用,并将这些对象所占用的内存空间标记为可回收状态,然后将这些内存空间重新分配给新的对象使用。
通过观察垃圾回收前后的内存使用情况,我们可以判断应用程序是否存在内存泄漏问题。如果在垃圾回收后,内存使用量并没有明显下降,或者持续增长,那么可能存在对象没有被正确释放,导致内存泄漏。这时,我们可以结合其他 Arthas 命令,如heapdump命令生成堆转储文件,再使用工具如 VisualVM 对堆转储文件进行分析,找出内存泄漏的根源。
使用 vmtool 排查问题过程
初始状态检查
在确定使用 vmtool 工具来排查短信通道问题后,我们首先执行了如下命令,检查当前系统中短信通道实例的数量。
# 查询CommonSmppChannel类的实例数量
[arthas@9532]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express 'instances.length'
@Integer[5]
这条命令的含义是,使用 vmtool 工具,通过getInstances
操作,获取com.cly.sms.gateway.tcp.common.CommonSmppChannel
类的所有实例的数量。和我们系统中启用的通道数量对比发现多了一个通道实例。
增加参数二次处理
仅仅知道实例数量还不足以深入了解问题,为了获取更多关于通道的关键信息,我们需要对结果进行进一步的处理。这时候我们修改express表达式,用于提取每个通道的通道号以及对应的通道参数。新的命令如下:
[arthas@9532]$ vmtool --action getInstances --className com.cly.sms.gateway.tcp.common.CommonSmppChannel --express '@java.util.Arrays@stream(instances).iterator.{#this.gatewaySendConsumer.channel.channelParam.channelNo+ "-" + #this.gatewaySendConsumer.channel.channelParam.reportType}'
@ArrayList[
@String[300000140-1],
@String[300000141-1],
@String[300000139-0],
@String[140000001-1],
@String[300000139-1],
]
这个命令中的--express
参数后面跟着一个复杂的表达式。@java.util.Arrays@stream(instances).iterator
这部分是利用 Java 的流操作,将获取到的实例集合转换为流,并获取其迭代器 。然后通过{#this.gatewaySendConsumer.channel.channelParam.channelNo+ "-" + #this.gatewaySendConsumer.channel.channelParam.reportType}
这个表达式,提取每个实例中gatewaySendConsumer.channel.channelParam
对象的channelNo
(通道号)和reportType
(报告类型,即是否推送短信报告的参数),并将它们用 “-” 连接起来展示。执行这个命令后,我们得到了一系列通道号和对应的报告类型信息。从这些结果中,我们清晰地看到每个通道都对应着一个发送实例,并且每个实例都有其特定的通道号和报告类型参数。
对于上述结果我们发现300000139通道创建了两个发送实例,并且两个实例中的reportType不同,从而导致通过该通道发送的短信部分有报告部分没有报告。至此已经确定是创建了多个通道实例导致的问题,只需要针对通道启动的代码着重排查即可。
问题总结
通过这一系列使用 Arthas 中 vmtool 工具的排查操作,我们终于确定了问题的根源。线上短信系统中部分短信发送报告缺失的异常情况,是由于通道重启后原实例未被销毁,新实例又被启动,导致新老实例并存,并且不同实例拥有不同的参数配置 。这种混乱的实例状态和参数不一致,直接导致了短信报告推送行为的异常,部分按照新参数配置推送报告,部分则按照旧参数配置不推送报告。
既然已经明确了问题,那么接下来的方向就很清晰了。我们需要针对通道重启这个关键操作进行深入排查,重点检查系统中与通道重启相关的代码逻辑。这其中,可能涉及到通道关闭逻辑是否正确,是否存在资源未正确释放的情况,导致原实例无法正常销毁;新实例的创建过程是否有异常,是否在不应该创建的时候创建了新实例;以及在实例管理方面,是否缺乏有效的机制来保证一个通道只存在一个有效的实例 。通过对这些方面的深入分析和排查,我们有信心找出导致问题的具体代码位置和原因,从而制定出有效的解决方案,修复这个困扰我们的线上问题,确保短信系统能够稳定、准确地运行,按照预期推送所有的短信发送报告。