半夜两点,突然满屏的NullPointerException

大家想象这样一个场景:

上周二凌晨两点十七分,你的手机像被诅咒了一样开始狂震。

运维在群里@了你八次,最后一条是:“王哥,支付接口全挂了,用户付不了钱,客服电话已经炸了,你赶紧看看!”

你迷迷糊糊摸到电脑,远程连上服务器,tail -f 看了半天日志,满屏的红色报错里就仨字:NullPointerException。

没了。

哪个对象是null?谁调用了谁?日志里一句多余的话都没有。你翻了一百多行,发现报错在OrderService的pay方法,但具体哪一行?不知道。参数是什么?不知道。用户当时操作了什么?不知道。

你盯着屏幕,感觉自己像个侦探,面对一堆碎片,却拼不出凶手的脸。

一个小时后,运维问你:“怎么样?定位到了吗?”

你说:“快了快了……”

其实你心里在骂:快个屁,我连门都没摸着。

后来一个老朋友甩给你一句:“你用Arthas啊,watch一下那个方法,参数返回值全能看到,比你看日志强一百倍。”

你抱着试试看的心态装上,一条watch命令下去,三秒后我就看到了那个null参数的来源——前端传过来的userId是空的,而代码里直接用了userId.toString(),没判空。

改完代码,重启,三分钟搞定。

你对着屏幕愣了半天,然后默默把Arthas加入了“保命工具箱”的第一位。

今天我就把这套“救急三板斧”拆开了揉碎了讲。你不用记复杂配置,不用改代码重启,一条命令就能把线上跑着的Java程序扒个底朝天。

一、Arthas是什么?就是Java程序的“照妖镜”

你可以把Arthas想象成给Java进程做“无痛胃镜”的工具。不用开刀(不用重启),不用麻醉(零侵入),伸进去看一眼,里面啥情况一目了然。

以前查线上问题,你得:

  1. 翻日志(有时日志压根没打)
  2. 看监控(只能看宏观指标)
  3. 猜代码(本地和线上可能不一致)
  4. 改代码加日志,重新发版,等半天

这套流程下来,天都亮了。

而Arthas直接附着在运行中的JVM上,让你能:

  • 实时看线程在干啥
  • 反编译线上类确认代码版本
  • 监控方法的入参、返回值、异常
  • 追踪调用链路耗时
  • 导出内存快照分析泄漏

关键是——一行代码都不用改,不用重启服务。生产环境最怕重启,Arthas完美绕过了这个痛点。

二、5分钟安装,比点外卖还快

不管你是Windows、Linux还是Mac,三步搞定。

第一步:确认Java环境
java -version

有输出版本号就行,没有就去装。

第二步:下载Arthas启动包

Linux/Mac:

curl -O https://arthas.aliyun.com/arthas-boot.jar
# 或者 wget https://arthas.aliyun.com/arthas-boot.jar

Windows(CMD或PowerShell):

powershell -Command "(New-Object System.Net.WebClient).DownloadFile('https://arthas.aliyun.com/arthas-boot.jar', 'arthas-boot.jar')"
第三步:启动并附着到目标进程
java -jar arthas-boot.jar

    它会列出所有Java进程,输入编号,回车,看到[arthas@pid]$就成功了。

    如果端口被占,指定其他端口:

    java -jar arthas-boot.jar --telnet-port 8564 --http-port 8565

    三、六个救命命令,搞定90%的生产事故

    命令1:dashboard——给系统做个体检

    直接输入dashboard,会看到一个实时刷新的面板:

    ID     NAME                   GROUP   STATE    %CPU
    12     http-nio-8080-exec-1   main    RUNNABLE 92.3
    3      Finalizer              system  WAITING  0.0

      重点关注:

      • %CPU列:哪个线程占用高,它就是“嫌疑人”
      • MEMORY:内存使用率、GC次数
      • STATE:有没有BLOCKED状态的线程(死锁嫌疑)

      按Ctrl+C退出。

      实战:CPU飙高怎么办?

      假设dashboard显示http-nio-8080-exec-5这个线程CPU占用85%,接下来用thread命令扒它。

      命令2:thread——把线程的底裤都扒出来

      看指定线程的堆栈

      thread 12   # 12是线程ID

      输出会显示这个线程正在执行的代码调用链,精确到类名、方法名、行号。

      比如你看到:

      at com.example.service.PaymentService.calculateFee(PaymentService.java:42)

      去查第42行,发现是个循环计算,循环次数没控制好,导致CPU飙高。

      检测死锁

      thread -d

      如果有死锁,会清楚列出哪个线程持有哪个锁、在等哪个锁,甚至指出代码行号。

      看线程状态分布

      thread -stats

      如果BLOCKED > 0,说明有锁竞争问题。

      命令3:jad——抓出“本地和线上代码不一致”的幽灵

      你有没有遇到过:本地跑得好好的,上线就报错。怀疑是代码没发版?直接反编译线上类看看。

      jad com.example.service.PaymentService

      输出反编译后的完整源码。

      还可以只看某个方法:

      jad -m calculateFee com.example.service.PaymentService

      甚至保存到文件:

      jad com.example.service.PaymentService > PaymentService.java

      我遇到过最离谱的一次:本地代码里有个null判断,线上反编译出来竟然没有——原因是同事发版的时候传了旧包。有了jad,三秒钟破案。

      命令4:watch——给方法装个“行车记录仪”

      这是我最常用的命令,相当于在方法入口和出口装了个摄像头,把参数、返回值、异常全拍下来。

      监控参数和返回值

      watch com.example.service.PaymentService pay "{params, returnObj}" -x 2

      当有人调用pay方法时,会输出:

      ts=2024-05-20 15:30:00; [cost=5ms]
      result=@ArrayList[
          @Object[][
              @Long[12345],      // 参数1:userId
              @BigDecimal[99.8], // 参数2:amount
          ],
          @Boolean[true],        // 返回值:true
      ]

      监控异常

      watch com.example.service.PaymentService pay "{params, throwExp}" -x 2

      如果抛异常,会看到异常类型和消息,比如:

      throwExp=@NullPointerException[
          message=null,
          stackTrace=[...]
      ]

      按条件过滤:只想看特定参数的调用:

      watch com.example.service.PaymentService pay "{params, returnObj}" -x 2 "params[0]==12345"

      只想看耗时超过100ms的调用:

      watch com.example.service.PaymentService pay "{params, returnObj, cost}" -x 2 "cost>100"

      ⚠️ 注意:watch会拦截方法调用,高频方法不要长时间监控,用完即关。

      命令5:trace——找出“谁拖慢了后腿”

      一个方法慢,但不知道是内部哪个子方法慢。用trace追踪调用链路。

      trace com.example.service.OrderService createOrder

        输出:

        ts=2024-05-20 16:00:00; [cost=500ms] OrderService.createOrder()
        `---[450ms] StockService.checkStock()        # 占了90%时间
            `---[448ms] StockMapper.selectStock()    # 慢在SQL查询
        `---[30ms] PriceService.calculatePrice()
        `---[20ms] OrderMapper.insertOrder()

        一眼看出是selectStock这个SQL慢,赶紧去优化索引。

        只看耗时超过100ms的子调用:

        trace com.example.service.OrderService createOrder --cost 100
        命令6:heapdump——拍个内存“CT”排查泄漏

        怀疑内存泄漏?导出堆快照分析。

        heapdump /tmp/heap.hprof

        然后下载到本地,用MAT或JProfiler打开,分析哪些对象占内存最大、为什么没释放。

        注意:导出会消耗资源,选业务低峰期,确保磁盘空间足够。

        四、进阶小技巧:让Arthas更好用

        用Web控制台

        启动后访问http://服务器IP:8565,图形化界面,命令自动补全,结果更直观。

        自定义别名

        把长命令缩短:

        alias watchPay='watch com.example.service.PaymentService pay "{params,returnObj}" -x 2'

        之后直接输watchPay

        OGNL表达式

        更灵活的过滤:

        watch com.example.service.UserService update "{params[0].age, returnObj}" -x 2 "params[0].age > 30"

        五、避坑指南(都是血泪教训)

        1. 别在高频方法上长期watch:性能损耗虽小,但每秒几万次调用还是会有影响。定位完问题立刻Ctrl+C关掉。
        2. heapdump要选对时机:别在业务高峰期执行,文件可能几GB,确保磁盘有空间。
        3. 权限问题:启动Arthas的用户要和Java进程的用户一致,否则连不上。
        4. Arthas是诊断工具,不是监控工具:用完就退,别常驻。监控用Prometheus+Grafana。

        六、来个完整实战:深夜救火全过程

        场景:支付接口偶发超时,日志只报“调用第三方超时”,没有更多信息。

        步骤1:dashboard看看整体情况,发现支付线程偶尔CPU飙升。

        步骤2:thread找高CPU线程,堆栈显示卡在HttpClient.execute

        步骤3:watch监控PaymentService.doPay方法的参数和返回值:

        watch com.example.service.PaymentService doPay "{params, returnObj, cost}" -x 2 "cost>1000"

        等了一会儿,抓到一条耗时3000ms的调用,发现参数里有个timeout=500,而正常应该是5000——原来是前端传了个错误值。

        步骤4:jad反编译前端调用的接口类,确认参数映射是否正确,发现前端字段名写错了,传了一个默认值。

        步骤5:通知前端修正字段名,重新发版。问题解决,全程不到半小时。

        如果没有Arthas,你可能要翻半天日志、猜参数、查代码,至少两小时。

        七、总结

        Arthas不是银弹,但它确实能把“半夜爬起来查问题”的恐惧感降低一半。以前我遇到线上问题先慌,现在我先掏出Arthas,三分钟定位不了再说。

        它就像一个随身带着的“解码器”,把JVM里那些黑盒信息一条条翻译给你看。

        下次你再遇到诡异bug,不妨试试:curl -O https://arthas.aliyun.com/arthas-boot.jar && java -jar arthas-boot.jar

        然后一条watch下去,问题可能就照妖镜面前现原形了。

        毕竟,程序员的命也是命,能少熬一次夜,就多喝一杯咖啡。

        评论
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

        当前余额3.43前往充值 >
        需支付:10.00
        成就一亿技术人!
        领取后你会自动成为博主和红包主的粉丝 规则
        hope_wisdom
        发出的红包
        实付
        使用余额支付
        点击重新获取
        扫码支付
        钱包余额 0

        抵扣说明:

        1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
        2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

        余额充值