线上某个 Java 服务突然出现诡异现象:接口超时、内存曲线像坐了火箭、日志里全是乱码。你第一反应是打开 IDE 连上去看,结果发现排查机器是内网隔离,别说 IDE,连远程调试端口都开不了;重启大法倒是能止血,但重启完问题可能就再也抓不到了。这种时候,我一般会直接退出"重启解决一切"的思路,改用 Arthas 这个 Java 诊断工具挂到目标进程上,在不重启、不改代码的前提下,把运行时的系统属性和堆内存状态摸清楚。今天这篇就专门讲 Arthas 里两个非常实用、又经常被低估的命令:sysprop和heapdump,结合我自己实际排查过的场景,把用法、原理和坑都拆开讲一遍。适合正在搞 Java 应用运维、或者遇到线上问题但不知道从哪里下手的开发者参考。
1. 为什么偏偏是这两个命令:先搞清楚它们的定位
1.1 生产环境里的"黑盒"困境
很多人第一次接触线上排查时,会下意识把问题分成两类:一类是"配置不对",一类是"内存不够"。这两个方向各自都有传统的排查手段,比如查启动脚本里的-D参数、用jmap -dump导堆快照。可真到了生产环境,事情没这么简单。
先说配置这一类。Java 的配置信息里很大一部分是系统属性(System Properties),也就是System.getProperties()拿到的那张键值表。它跟启动参数(-Dkey=value)、环境变量(Environment Variables)是三兄弟,但经常被混为一谈。有些问题,比如file.encoding不对导致读文件乱码、java.io.tmpdir指向了一个不可写的目录导致上传临时文件失败、user.timezone跟业务期望的时区差了 8 个小时,都是系统属性埋的雷。但麻烦的是,这些属性在进程启动之后就被"焊死"了一部分,你从外面看启动脚本未必能发现问题,因为值可能是在代码里用System.setProperty改过的。
再说内存这一类。内存问题最怕的是"事发现场已经没了"。等你发现内存飙升,再去翻监控、猜代码,堆里到底是什么对象占着空间,早就说不清了。heapdump的价值就是把堆的"案发现场"完整拍下来,让你事后慢慢分析。
Arthas 的高明之处在于,它把这两类排查能力都塞进了一个命令行工具里,进程不用重启、业务代码不用动,attach 上去就能查。sysprop负责读改系统属性,heapdump负责导出堆快照,两把钥匙配合使用,能覆盖掉线上排查里相当一部分"症状在表面、病根在深处"的情况。
1.2 这两个命令解决了什么核心痛点
我想用一个表格先概括一下它们的定位,后面再各自展开:
| 维度 | sysprop | heapdump |
|---|---|---|
| 作用对象 | JVM 系统属性表 | JVM 堆内存 |
| 核心操作 | 查看、按 key 过滤、修改 | 生成 hprof 堆快照文件 |
| 典型症状 | 乱码、时区错、临时目录不可用 | OOM、GC 频繁、内存缓慢上涨 |
| 排查特点 | 实时、轻量、可反向验证 | 离线分析、信息量大 |
| 风险等级 | 低(修改需谨慎) | 中(大堆时会有停顿) |
从我自己的经验看,这两个命令最适合的组合套路是:症状比较"外在"的时候先上sysprop,用最小代价确认配置有没有问题;症状指向内存时再上heapdump,把怀疑范围缩小到具体的类和对象。下面我按顺序讲透。
2. sysprop 命令实战:读取与修改 JVM 系统属性
2.1 基础用法:先看全量,再按 key 定向查询
sysprop的使用非常直接。进入 Arthas 交互界面后,直接敲sysprop,会打印出当前 JVM 里全部的系统属性。在我的机器上一个中等规模的 Spring Boot 应用大概会打出两三百行,包括java.version、java.home、os.name、user.dir、file.encoding这些常见项,也包括中间件、框架在启动时写入的自定义属性。
几百行看着眼花,实战中我很少真的去翻全量输出,更多是带着疑问定向查。命令格式是sysprop <key>,比如:
sysprop file.encoding sysprop user.timezone sysprop java.io.tmpdir输出会直接显示 key 对应的 value。如果这个 key 不存在,Arthas 会提示查不到,这时候要想想是不是拼写问题,或者这个属性根本就不是系统属性,而是环境变量或应用自定义配置,别在sysprop上死磕。
有一个细节值得注意:sysprop输出的是"当前运行时"的系统属性,而不是启动脚本里写了什么。这意味着它能反映代码里System.setProperty之后的状态。之前我遇到过一个问题,启动脚本里明明写了-Dfile.encoding=UTF-8,但应用日志还是输出 GBK 乱码。用sysprop file.encoding一看,运行时值居然是GBK,说明有人在代码里重新 set 过。这种"启动参数被覆盖"的隐蔽场景,只有运行时查看才能发现。
2.2 修改属性:语法很简单,生效范围要拎清
sysprop不仅能读,还能改。语法是在 key 后面直接跟新值:
sysprop user.timezone Asia/Shanghai sysprop file.encoding UTF-8执行成功后会返回修改后的值。这里必须强调三个容易被忽视的点。
第一,修改是临时的,只对当前进程生效,进程重启后就会丢。它本质上是调用了System.setProperty,改的是运行内存里的属性表,不会回写启动脚本,也不会持久化到任何配置文件。所以别指望用它来"修复"配置,更合适的定位是"临时验证"。
第二,很多属性在 JVM 启动早期就被读取并缓存到了其他变量里,sysprop改了之后未必能立刻影响已经初始化的组件。举个例子,file.encoding这个属性比较特殊,JDK 在启动时会根据它初始化 Charset 相关的内部状态,运行时再改file.encoding,已经创建好的 Reader/Writer 和某些编码转换逻辑可能还是老样子。所以如果你想验证"改成 UTF-8 之后乱码是否消失",最可靠的做法是改完再触发一次相关业务路径,观察新创建的流是否正常。
第三,修改时要克制。sysprop修改属性的操作是有副作用的,尤其是在并发环境下,一个属性被多个线程读取,你改了值可能会导致不可预知的行为。我在生产上只会在"影响面可控、能快速回滚(重启即恢复)"的情况下用它,更多时候拿它来做理论验证,而不是长期修复手段。
2.3 实战场景:一次由 file.encoding 引发的乱码排查
为了让这块内容更具体,我讲一个之前处理过的案例。某业务系统在处理用户上传的 CSV 文件时,解析出来的中文全是乱码。常规怀疑对象是代码里读取文件时没有指定字符集,但翻代码没发现明显问题。我挂上 Arthas,先查了运行时属性:
sysprop file.encoding sysprop sun.jnu.encoding结果是file.encoding=GBK,sun.jnu.encoding=UTF-8。sun.jnu.encoding影响的是文件名的编码,file.encoding影响的是文件内容的默认读写编码。这两个值不一致,在某些 JDK 版本下就很容易出现"文件名正常、内容乱码"的现象。问题的根源是某位同事在公共初始化代码里加了System.setProperty("file.encoding", "GBK"),本来想兼容老系统,结果殃及池鱼。
我当时没有直接改代码上线,而是先用sysprop file.encoding UTF-8在运行态改掉,再让业务侧重新上传一次文件,确认乱码消失。验证通过后才去改代码、走发布流程。这就是sysprop最典型的用法:不重启、不改代码,先证明"病因在这里",再决定怎么治。
2.4 与 jinfo 的对比:Arthas 强在哪
说到看系统属性,可能有人会提 JDK 自带的jinfo。确实,jinfo -sysprops <pid>也能打印系统属性,而且不用装 Arthas。但我个人的体感是,在交互式排查场景里 Arthas 的sysprop更顺手:
jinfo是一次性命令,查一个 key 要敲一遍完整命令,sysprop在 Arthas 交互界面里可以连续敲好几条,上下文不丢。- Arthas 支持 key 前缀补全,敲到一半按 Tab 会提示有哪些属性名,不用死记硬背。这个体验在记不清
sun.jnu.encoding这种冷门 key 的时候特别救命。 - 修改属性的能力也是
sysprop的加分项,虽然jinfo在特定场景也能改某些 flag,但系统属性的运行时修改,用 Arthas 更直接。
3. heapdump 命令实战:把堆内存拍成一张"现场照片"
3.1 基本用法与输出位置
heapdump的用途是导出 JVM 堆的 hprof 二进制快照。进入 Arthas 后,最简单的用法是:
heapdump这会在当前工作目录生成一个类似java_pid12345.hprof的文件。我建议你手动指定路径,免得 Arthas 的工作目录跟预期不一致,文件生成之后找不到:
heapdump /data/dump/20240615-order-service.hprof还可以通过--live参数只导出存活对象:
heapdump --live /data/dump/20240615-order-service-live.hprof--live的含义是触发一次 Full GC,把堆里的垃圾对象清掉之后,只保留存活对象再 dump。这样导出的文件更小,分析的时候也不会被大量无用的死对象干扰。但我必须把丑话说在前面:--live会带来一次 Full GC 停顿,在堆特别大、比如十几个 GB 的实例上,这个停顿可能长达数秒甚至更久,对线上业务有明显影响。所以我的经验法则是——如果只是怀疑内存泄漏、怀疑大对象,优先不加--live,先导一个完整快照;只有在堆文件实在太大、分析工具打不开,或者你能接受一次短暂停顿的时候,才考虑--live。
3.2 和 jmap 相比,heapdump 命令的差异化优势
传统导出堆快照用得最多的是jmap -dump:format=b,file=xxx.hprof <pid>。那 Arthas 的heapdump还有什么存在的必要?我的答案是:方便,以及安全。
第一,jmap对导出时机的掌控能力比较弱,经常是命令行一执行立刻 dump,缺少跟业务侧的协调空间。而在 Arthas 交互界面里,你可以先观察内存变化趋势,挑一个相对空闲的时机执行heapdump,或者先分析一下对象的增长情况再决定要不要 dump。
第二,Arthas 的heapdump可以配合它自己的其他命令快速做"dump 前判断"。比如先用memory命令看堆内存各个区域的使用率,再用dashboard看线程和内存概况,确认确实需要堆快照了,再执行 dump。整套动作在一个会话里完成,中间不用切工具,排查思路不断档。
第三,从生产权限角度来说,某些环境只开放了应用端口,不开放 SSH 直连或者没有 JDK 的jmap权限,但允许通过 web 终端进入 Arthas 之类的诊断通道。这种时候heapdump可能是你唯一能触达堆的方式。
3.3 拿到 hprof 之后,怎么分析才高效
生成 hprof 只是开始,真正的排查从打开文件才刚开始。我常用的工具是 Eclipse MAT 和 JVisualVM,前者做内存泄漏分析非常顺手,后者看对象分布更直观。
用 MAT 打开 hprof 之后,我习惯按这个顺序走:
- 先看Overview页面的
Leak Suspects报告,MAT 会直接用柱状图告诉你哪些对象最可疑。 - 再看Histogram,按类名统计实例数量和 Shallow Heap / Retained Heap。Retained Heap 是"如果这个对象被回收,能释放多少内存",这个指标最能反映对象对内存的占用贡献。
- 对怀疑的类点右键,查看Merge Shortest Paths to GC Roots / Path to GC Roots,看看这个对象是被谁引用住的。如果是
Thread或者ThreadLocal引着不放,那基本就是线程上下文没清理;如果是某个静态集合引着,那就是缓存的锅。
之前排查一个内存缓慢上涨的问题时,我就是通过 Histogram 发现某个自定义缓存类有几十万个实例,Retained Heap 占了堆的一大半。再顺着 GC Roots 找上去,发现是定时任务里不断往一个静态ConcurrentHashMap塞数据,但清理逻辑因为一个异常分支永远走不到,最终内存只涨不降。这种结论,不 dump 堆单靠猜是根本定位不到的。
如果用 JVisualVM,打开 hprof 后可以直接看"对象"页面的实例数排行和"堆转储"的按类统计。它的好处是图形化做得比较亲切,适合快速人肉浏览;缺点是内存分析深度不如 MAT,复杂引用链还是得到 MAT 里抠。
3.4 一个典型场景:OOM 前夜的堆快照
再分享一个真实遇到过的场景。某个服务每天下午内存就蹭蹭往上涨,监控图显示是一个"锯齿波"——每天都在涨,夜里低谷也不回到原点,过几天就得重启一次。这种"缓慢泄漏"最折磨人。
我的排查流程是:先heapdump导出第一份快照,记下时间点;然后让服务继续跑,过几个小时内存涨到更高位再导第二份快照。把两份快照同时放进 MAT,用Compare Base / Compare Tables对比两个 Histogram,就能直接看到"这段时间里哪个类多出来了多少对象"。这种差值分析法比单看一份快照高效得多,因为它天然把"只增不减"的对象筛选出来,而这一类对象正是泄漏的高发区。
最后定位到是一个外部接口调用的响应对象被放进了本地队列,但因为消费线程在异常情况下没有执行remove,导致队列只进不出。解决方案是加finally清理、给队列加容量上限。整个过程里,heapdump给了我最硬核的证据,避免了跟业务团队反复拉扯"到底是哪段代码的问题"。
4. 双命令配合的完整排查链路:一个虚构但典型的案例
4.1 现象描述与初步判断
为了把sysprop和heapdump串起来讲,我构造一个综合案例来说明排查思路。假设有一个订单导出服务,用户反馈导出的 Excel 文件中文列名乱码,同时运维监控发现这个服务的内存占用在过去一周里缓慢上涨,GC 频率也越来越高。
这其实是两个互相独立的问题,但发生在一个进程里,就需要一套不重启的排查流程把它们分开确认。挂上 Arthas 之后,我通常会先跑两个快速命令摸底:
sysprop file.encoding memorysysprop确认字符集相关的属性值,memory看堆区、非堆区的整体水位。摸底之后,再决定下一步往哪走。
4.2 先用 sysprop 验证"乱码是不是配置问题"
按第 2 节的思路,先查file.encoding,发现值是GBK,而业务方期望的是 UTF-8。为了确认这确实是乱码的直接原因,我在运行态把它改成 UTF-8:
sysprop file.encoding UTF-8然后让测试同学重新触发一次导出,确认乱码消失。到这里,乱码问题的根因已经锁定,剩下的就是走代码修复流程:删掉启动时的错误setProperty,或者显式指定导出文件的字符集。
这里我要单独说一句:不要只满足于"改成 UTF-8 之后好了"。还要追问一句"为什么之前是 GBK",否则下次换环境可能复现。我当时就是顺着这条线往前查,才发现是某个公共依赖在初始化时写死了GBK,属于典型的"一个坏人拖垮全家"。
4.3 再用 heapdump 锁定内存上涨的元凶
乱码问题告一段落,内存问题继续查。先导一份基线快照:
heapdump /data/dump/order-export-baseline.hprof让服务继续跑一段时间后,再导第二份:
heapdump /data/dump/order-export-after2h.hprof用 MAT 的 Histogram 对比两份快照,发现有一个名叫ExportTaskContext的类实例数量从几千涨到了几十万,Retained Heap 增长了近一倍。顺着 GC Roots 看,这些对象全被一个静态的ThreadLocal变量引用着。再结合代码排查,发现导出任务在finally块里没有执行context.remove(),异常分支直接 return 导致 ThreadLocal 里的对象一直被当前线程持有,线程池里的线程又不会销毁,所以内存只涨不降。
这个案例里,sysprop和heapdump其实是两种完全不同类型的问题,但它们在同一个排查会话里无缝切换——这也是线上诊断工具最理想的状态:不需要为每个问题换一套工具链。
5. 实操中的高频坑与我的处理习惯
5.1 修改系统属性,改完一定要验证并恢复
sysprop改属性这件事,看起来简单,但它跟"改配置文件"的心智模型完全不同。配置文件的修改是持久的,而sysprop的修改是进程内的临时状态。一旦你忘了恢复,这个被改过的属性就会一直留在内存里,影响后续所有依赖它的逻辑。
我的习惯是:在执行修改前,先记下原始值;验证结束后,要么重启进程恢复,要么再通过sysprop <key> <原始值>改回去。尤其是user.timezone、file.encoding这种影响面广的属性,改完不恢复,很可能会把线上环境带入另一个坑。
另外,sysprop修改对某些框架可能无效甚至有害。比如框架在启动时已经把file.encoding读走并缓存到自己的配置对象里,你从外部改系统属性表,框架内部依然用旧值。这种情况下不要硬来,老老实实改代码和启动参数。
5.2 heapdump 的时机和文件管理,是最容易翻车的地方
heapdump最大的坑不是命令本身,而是"什么时候执行"和"导出的文件放哪"。大堆实例上,dump 过程本身要遍历堆对象,期间可能会导致应用响应变慢,尤其在不加--live时也要注意,虽然普通 dump 的停顿通常比--live小,但堆特别大时依然有明显的 CPU 和内存开销。所以我建议在业务低峰期执行,或者提前跟业务方打个招呼。
文件管理同样重要。一个几 GB 的堆,导出的 hprof 也可能接近这个量级,生产环境磁盘不够导致导出失败的情况我遇过不止一次。现在我的固定动作是:dump 前先df -h看磁盘剩余,再指定一个空间充足的绝对路径;dump 完立刻分析,分析完及时删除,避免 hprof 把磁盘塞满。
这里给一个我自己常用的命令顺序,供参考:
# 1. 看堆水位 memory # 2. 确认磁盘空间 # 在 Arthas 里可以执行 !df -h(外部命令) # 3. 导出完整堆快照 heapdump /data/dump/app-$(date +%Y%m%d%H%M).hprof # 4. 如果需要存活对象快照,单独导出 heapdump --live /data/dump/app-live.hprof5.3 敏感信息与权限管控
堆快照里有敏感数据,这个很多人会忽略。hprof 文件本质上是内存的二进制快照,里面可能包含用户手机号、身份证号、业务密钥、数据库连接串等一切驻留在堆里的信息。把 hprof 直接丢给第三方分析,或者上传到不安全的存储,都是很大的安全隐患。
我的处理习惯是:hprof 文件只在需要分析的人员范围内流转,分析完优先用安全删除方式处理;如果必须留档,会先做脱敏或者限制访问权限。Arthas 本身在进入交互会话时也建议做好权限控制,避免非授权人员 attach 到生产 JVM 上乱操作。
sysprop虽然不涉及堆内容,但它能暴露 JVM 启动参数和部分环境相关属性,对排查有帮助,但也算敏感面的一部分。总的原则是:诊断工具给了你很大的操作权限,权限越大越要自律,只做"必要的最小操作"。
5.4 两个命令的转储能力对比小结
最后用一个表格把这两个命令的关键差异再收拢一下,方便你以后快速查阅:
| 对比项 | sysprop | heapdump |
|---|---|---|
| 常用子命令 | 无子命令,靠参数区分读/写 | --live控制是否只导存活对象 |
| 执行耗时 | 毫秒级 | 秒级到分钟级,看堆大小 |
| 对业务影响 | 极小 | 可能有明显停顿,大堆更明显 |
| 输出物 | 控制台文本 | hprof 二进制文件 |
| 常用分析工具 | Arthas 控制台 | Eclipse MAT / JVisualVM |
| 后续恢复手段 | 手动改回或重启 | 无需恢复,但要及时清理文件 |
6. 写在最后的个人体会
sysprop和heapdump单独拿出来都不复杂,花十分钟看一遍官方文档就能上手,但真正让它们发挥价值的是使用时的克制和思路。sysprop适合做"快速验证"而不是"紧急修复",改之前想清楚影响面,改之后记得恢复;heapdump适合做"事后取证",别在症状发生时手忙脚乱,先导基线、再导对比,用差值去缩小怀疑范围,比对着一份快照瞎猜有效得多。
我自己在无数次线上排查里形成的习惯是:先花五分钟把所有能快速获得的运行时信息摸一遍,包括系统属性、内存水位、线程状态,再决定要不要上重型手段。很多时候问题在sysprop这一步就找到了,根本等不到heapdump出场。但一旦内存类问题真的出现,手里有一份高质量的 hprof,就等于有了跟同事、跟上级沟通时最硬气的证据。工具永远是辅助,真正值钱的是你脑子里那条清晰的排查链路。希望这篇能把这两个命令的用法和边界讲清楚,下次你在线上碰到乱码或者内存疑难杂症的时候,能少走几步弯路。