十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java 远程调试实战:Tomcat JDWP 与 IDEA 断点排查

Java 远程调试实战:Tomcat JDWP 与 IDEA 断点排查 1. 远程调试到底在解决什么问题凡是在本地写得爽、一上服务器就翻车的代码最后基本都会走到同一个动作上用 IDEA 的 Remote 模式挂到 Tomcat 上做远程 DEBUG。这不是什么高阶玩法而是分布式部署、容器化部署普及之后每个后端同学早晚要掌握的一项基本技能。它的核心价值在于代码跑在别处断点打在你熟悉的编辑器里变量、调用栈、表达式求值全都能看和本地调试几乎没有区别。我最早接触这套东西是因为一个只在测试环境复现的订单状态错乱问题本地怎么点都正常测试环境偶发日志里全是异步线程的输出顺序完全对不上。最后靠的就是把远程调试挂上去在状态机流转的那一行打断点看着线程栈一层层展开才定位到是一个定时任务和用户请求并发修改了同一个对象。如果当时只有日志这个问题我能查三天。所以这篇文章我想讲清楚三件事Tomcat 这一端怎么把调试端口开出来IDEA 这一端怎么配 Remote 连接以及真正连上之后那些文档里不会写的坑比如断点是空心的、类被重新加载、连上了但停不住等等。内容适合已经会用 IDEA、但没怎么在生产或测试服务器上做过远程调试的同学也适合被 Connection refused 折磨过的老手来对照排查。1.1 一个只在服务器上翻车的 Bug 长什么样远程调试真正的主场是那些本地无法复现的问题。典型的有这么几类。第一类是环境差异JDK 版本不同、操作系统的文件路径分隔符不同、时区不同、字符集不同导致同样的代码在不同机器上走出不同分支。第二类是配置差异数据源指向不同、连接池参数不同、缓存中间件不同问题只在大压力或者特定配置下暴露。第三类是并发与时序本地单机单请求服务器上几十个线程一起跑只有在那里才能触发竞态。这几类问题的共同点是——你没法通过在本地多跑几次来解决因为触发条件压根不在本地。这时候能用的手段就三种加日志、远程调试、看监控和线程栈快照。加日志最稳但最慢改一次发一次一个问题可能来回发五六次版本远程调试最快但需要能连上目标机器、且对运行环境有一定侵入看线程栈适合分析卡死和死锁但不适合追踪业务逻辑的中间状态。我个人的判断标准很简单如果这个问题我能用不超过两轮日志定位那就加日志如果两轮日志之后还是雾里看花那就别犹豫直接开远程调试。因为反复发版的沟通成本和时间成本远比开一次调试端口的成本高。1.2 JDWP 的工作方式把 JVM 的调试接口当成一个插座要理解远程调试先要理解 JDWP也就是 Java Debug Wire ProtocolJava 调试线协议。几乎所有 Java 的调试工具不管是 IDEA、Eclipse 还是命令行的 jdb本质都是通过这套协议和 JVM 通信。可以把它类比成墙上的插座JVM 启动的时候如果加上了-agentlib:jdwp...这串参数就相当于在自己身上装了一个插座并且把插座的位置地址和端口广播出去。调试器则是一根插头通过 TCP 连接到这个插座上之后双方用一套约定好的格式交换信息——下断点、查询线程列表、读取变量值、单步执行全部走这条连接。这里有两个关键角色需要分清。servery表示 JVM 是服务端它监听端口等调试器来连这是最常见的用法也就是 IDEA 里的 Attach to remote JVM。servern表示 JVM 反过来去连调试器对应 IDEA 里的 Listen to remote JVM。另外还有suspend参数它决定 JVM 启动时是直接跑suspendn还是停下来等调试器接入suspendy。这两个参数组合起来就决定了整个调试的握手方式。理解这一点之后很多问题就顺了。比如为什么我点 Debug 显示连接被拒绝那是因为 JVM 端的插座没装上或者端口写错了为什么 Tomcat 启动卡住不动了那多半是suspendy在等调试器。1.3 三种排障手段的取舍对比实际工作中怎么选我整理了一张对照表这是我踩过坑之后形成的习惯手段侵入性见效速度适用场景主要限制加日志打点低但要改代码重发版慢取决于发版流程逻辑分支清晰、状态可枚举的问题需要反复迭代日志量难控制远程 DEBUG中需要开调试端口快实时看变量本地无法复现、需要看运行时状态需要网络可达有性能与安全成本线程栈与堆快照低不改代码中需要解读成本卡死、死锁、内存泄漏看不到历史执行路径需要强调的是远程 DEBUG 是有性能代价的。JVM 在调试模式下尤其是有断点被命中的时候相关线程会挂起如果断点打在高频方法上整个应用的吞吐会立刻掉下来。所以我的原则是调试端口只在需要的时候开调试完立刻关掉绝对不在生产环境的公网入口上开着。1.4 开搞之前必须先落实的四件事在动手改配置之前有四件事要先确认否则后面会在各种奇怪的地方浪费时间。第一目标机器上的 Java 进程是不是用标准 JVM 启动的。有些容器镜像或者托管平台对启动参数做过包装你加的参数可能被覆盖掉。第二你本机到目标机器的端口是否可达这一点在内网机器之间通常没问题跨网段就要提前确认别配了半天发现是网络不通。第三源码版本必须和服务器上跑的包完全一致包括分支和提交号否则断点行号对不上会出现断点打上去是空心的这种迷惑现象。第四你是否有权限重启这个应用因为改启动参数必然要重启一次。注意调试端口本身不带任何鉴权能连上就等于能读取和修改运行时内存等于拿到了这个进程的代码执行能力。所以它只能在受信任的内网、只对明确的调试来源机器开放绝不能和业务端口一样对外暴露。2. Tomcat 侧怎么把调试端口开出来Tomcat 这一端的核心任务只有一句话给 JVM 加上 JDWP 参数。但在哪里加这个问题答案比想象中多选错了会导致参数不生效或者升级 Tomcat 版本之后配置被覆盖。下面把我用过的几种方式挨个说清楚。2.1 别直接改 startup.sh优先用 setenv.shTomcat 的启动脚本体系是这样的startup.sh调用catalina.shcatalina.sh负责组装 JVM 参数和环境变量。直接在startup.sh或者catalina.sh里硬改当然可以但问题是版本升级、重新解压部署的时候这些改动会被覆盖你不一定记得自己改过什么。更稳妥的做法是在$CATALINA_BASE/bin/目录下新建一个setenv.shWindows 下是setenv.bat。catalina.sh在启动时会自动去 source 这个文件如果它存在的话。这个文件不随 Tomcat 发行包一起发布所以升级时天然不会被覆盖也方便做环境差异化——开发机一份、测试机一份。# $CATALINA_BASE/bin/setenv.sh export CATALINA_OPTS$CATALINA_OPTS -agentlib:jdwptransportdt_socket,servery,suspendn,address*:5005记得给执行权限chmod x setenv.sh。这一步看起来多余但我见过不止一次因为忘记加权限导致参数没生效、排查半天的案例。2.2 JAVA_OPTS 和 CATALINA_OPTS 的区别以及为什么优先选后者这两个变量经常让人犯迷糊因为在多数场景下它们的效果看起来一样。区别在于JAVA_OPTS会被 Tomcat 的启动、停止、版本查看等所有命令使用而CATALINA_OPTS只在真正启动 Tomcat 实例start、run时使用。对一个调试端口来说你显然不希望执行shutdown.sh的时候也去监听 5005——那会导致关闭脚本莫名其妙地卡住或者报端口占用。所以我更推荐写进CATALINA_OPTS。网上还有一种写法是设置JPDA_OPTS它和catalina.sh jpda这个子命令配合使用。这属于另一种路子下一节展开。2.3 JDK 8 和 JDK 9 的参数写法差异这是最容易踩的坑JDWP 参数的语法在不同 JDK 版本上有实质区别写错了不会报错而是连不上非常难查。JDK 8 及更早版本-agentlib:jdwptransportdt_socket,servery,suspendn,address5005JDK 9 及更高版本-agentlib:jdwptransportdt_socket,servery,suspendn,address*:5005区别就在address上。从 JDK 9 开始如果只写端口号JVM 默认只绑定在回环地址 127.0.0.1 上也就是说只有本机能连远程的 IDEA 永远连不上而端口检测工具在服务器本机跑又是通的极具迷惑性。要允许外部连接必须写成*:5005或者0.0.0.0:5005。我个人的习惯是不管用哪个版本的 JDK只要需要远程连一律写address*:5005。如果项目还在跑 JDK 8 且这么写报错再退回只写端口号。如果你还在用 JDK 5 到 8 之间的老写法-Xdebug -Xrunjdwp:...虽然大部分情况还能用但已经被标记为过时建议统一换成-agentlib:jdwp的形式。2.4 用 catalina.sh jpda 的快捷方式以及它的默认值陷阱Tomcat 自带一个便捷入口sh catalina.sh jpda start它会自动读取JPDA_TRANSPORT、JPDA_ADDRESS、JPDA_SUSPEND这几个环境变量拼出 JDWP 参数。默认值大致是传输方式dt_socket、地址localhost:8000、suspendn。注意这里的默认地址是localhost:8000不是所有网卡。这意味着你在服务器本机用telnet localhost 8000是通的但远程连一定失败。要开放给外部需要在setenv.sh里显式覆盖export JPDA_ADDRESS*:8000 export JPDA_SUSPENDn这个默认值我强烈建议你自己去grep -n JPDA_ADDRESS catalina.sh确认一次因为不同大版本的 Tomcat 默认值写法确实有差异以你机器上的脚本为准最保险。2.5 端口、绑定地址与防火墙这三重门参数写对只是第一步端口能不能被外面连上还要过防火墙这一关。排查顺序建议从上到下先确认进程确实在监听# Linux ss -lntp | grep 5005 netstat -anp | grep 5005REM Windows netstat -ano | findstr 5005如果Local Address那一列显示的是127.0.0.1:5005说明绑定地址还是回环回到上一节检查address参数。如果显示0.0.0.0:5005或者*:5005那就说明 JVM 端没问题问题在链路上。接着从你的开发机做连通性测试nc -vz 192.168.1.100 5005 # 或者 telnet 192.168.1.100 5005# Windows PowerShell Test-NetConnection -ComputerName 192.168.1.100 -Port 5005不通就是防火墙或安全组的问题按需放行来源 IP不要图省事直接对所有地址开放。提示调试端口用完之后最省事的关闭方式不是去改配置文件而是直接在调试完的应用上重启或者把调试参数注释掉再重启。但如果你是在临时排障、不想重启业务至少要在防火墙层面把来源限制收紧。我个人是养成了调试结束就顺手把 setenv.sh 里那行注释掉的习惯避免哪天忘了。3. IDEA Remote JVM Debug 配置全流程环境这一端搞定接下来是 IDEA 这一端。不同版本的 IDEA 界面措辞略有差异新版叫 Remote JVM Debug老版本叫 Remote路径和参数基本一致。3.1 新建一个 Remote JVM Debug 配置打开Run菜单进入Edit Configurations点左上角的在列表里找Remote JVM Debug。如果你是第一次配可能会在列表里看到好几个名字很像的选项比如 Remote JVM Debug、Remote、Tomcat Server注意别选成 Tomcat Server那个是本地部署用的完全不是一回事。新建之后你会看到一个很简洁的面板主要就这么几项Name、Host、Port、Command line arguments、Use module classpath、Debugger mode 和超时设置。建议把 Name 改成一个能一眼看懂业务的名字比如order-service-测试环境-5005。同一台机器上挂多个服务的调试端口是常态命名混乱的话一周之后你自己都想不起来哪个是哪个。3.2 Host、Port、Debugger mode 到底怎么填Host目标 JVM 所在机器的 IP 或域名。不要填localhost除非被调试的进程就在你本机。Port和 Tomcat 端address里的端口保持一致比如 5005。Debugger mode绝大多数情况选Attach to remote JVM即调试器主动去连 JVM对应 JVM 端的servery。只有在 JVM 端写的是servern时才选Listen to remote JVM。Use module classpath选到包含被调试业务代码的模块。这一项很关键后面单独说。超时时间默认值一般够用如果目标机器响应慢可以适当调大。一个常见困惑是为什么 IDEA 生成的命令行参数和我写的顺序不太一样其实 JDWP 参数的顺序不敏感只要键值对正确就行不用纠结。3.3 Command line arguments 这串参数是怎么拼出来的配置面板上会显示一行供你复制的命令行参数大致长这样-agentlib:jdwptransportdt_socket,servery,suspendn,address*:5005拆开看每一项的含义transportdt_socket表示走 TCP socketservery表示 JVM 当服务端监听suspendn表示不等调试器就直接启动address*:5005是监听地址和端口。如果你的需求是连 Tomcat 启动阶段初始化代码里的问题那就把suspend改成y。这时候 JVM 一启动就会停住直到 IDEA 连上来才继续往下跑从类加载到 Spring 容器初始化每一步都能断到。代价是如果 IDEA 一直没连上Tomcat 就一直卡在那里不提供服务。实操顺序上suspendy时建议先启动 Tomcat看着它打印出Listening for transport dt_socket at address: 5005之后再在 IDEA 里点 Debug 连上去。反过来先点 IDEA 也不是不行只是目标还没起来的时候 IDEA 会一直重试直到超时体验差一点。3.4 Use module classpath 与源码版本对齐这一项经常被忽略但它是断点能不能正常停下来的关键。IDEA 通过这个选项知道该用哪份本地编译产物和源码去匹配远端的类。如果选错了模块或者源码版本对不上典型表现是断点打下去变成一个灰色的空心圆鼠标悬停提示类似 No executable code found at line xxx。我的做法是固定的三步走先从服务器上确认部署包对应的 Git 提交号把自己本地分支切到同一个提交重新 build 一次再在配置里把 module classpath 选到这个模块。省这一步的后果就是你会对着一堆断点不生效怀疑人生而问题根本不在调试配置上。一个折中方案是如果服务器上的包是某个固定版本而你在本地为了排查已经改了代码那就在本地新建一个 worktree 或者重新 clone 一份对得上的版本专门用来调试。听起来麻烦但比对着错行的代码瞎猜要高效得多。3.5 连上没连上怎么一眼看出来连接成功有两个很明确的信号。一是 Tomcat 端的启动日志里会有这么一行Listening for transport dt_socket at address: 5005看到它说明 JDWP 参数被 JVM 正确识别了插座装上了。二是 IDEA 的 Debug 控制台里会出现Connected to the target VM, address: 192.168.1.100:5005, transport: socket看到这行就说明插头进去了可以开始打断点。如果 IDEA 报的是连接被拒绝说明目标端口没人监听回到第二章检查参数是否生效、进程是否真的重启过。如果是连接超时多半是链路上被拦住了。这两种错误的排查方向完全不同先看清楚报的是哪一种能省下不少时间。4. 实测踩坑与问题排查速查这一章是我这些年攒下来的问题清单基本都是官方文档不会提、但实际一定会遇到的。4.1 常见现象与原因对照表现象大概率原因处理方向Connection refused参数没生效、进程没重启、端口写错查启动日志里的 dt_socket 行Connection timed out防火墙、安全组、地址绑定在回环检查绑定地址与放行规则连上了但断点全灰源码版本不一致切换到与部署包一致的提交并重新编译断点打在高频方法上应用卡死断点命中导致线程挂起改条件断点或换低频率入口断点第一次有效后面失效类被新 ClassLoader 重新加载重新 attach或关掉自动重载单步进入一堆框架代码调用链上都是代理用条件断点或异常断点缩小范围这张表我自己放在笔记里遇到问题先对照一遍通常五分钟内能定位方向。4.2 断点灰色空心、提示 No executable code found这是远程调试里最高频的问题没有之一。它的本质只有一个IDEA 认为你打断点的这个位置和远端实际运行的字节码对不上。常见的三种具体原因源码分支不对服务器上跑的是 A 分支你本地是 B 分支源码版本对但没重新编译IDEA 用的还是旧的 class 缓存代码经过了字节码增强或者 AOP 代理比如用了某些编译期织入的框架导致行号信息偏移。处理顺序建议是先确认提交号再Rebuild一次对应模块再看断点是否还是灰的。如果前三步都做了还是灰的就换一个稳定不会被增强的位置打比如 Controller 的方法入口第一行先验证调试链路本身是通的再往深处走。提示有些框架在启动时会做类加载期的字节码转换这类代码无论你怎么对齐版本都可能断不准。遇到这种情况别硬碰改用在关键位置加一行日志、配合调试看后续调用链的方式效率更高。4.3 断点停不住、类被重新加载Tomcat 有个很常见的配置项叫自动重载也就是检测到 class 文件变化就重新加载整个 Web 应用。这在开发时很方便但在调试时会带来麻烦应用被重载之后原有的类由一个新的 ClassLoader 加载之前注册的断点就挂在了旧类上看起来断点还在但永远不停。判断方法很简单观察 Tomcat 日志里有没有出现重新加载应用的相关输出。如果有说明你调试的这段时间里应用被重载过。处理方式有两种一是在调试期间临时把这个自动重载关掉重启后再调二是确认重载发生后在 IDEA 里断开重连一次让它重新注册断点。我一般选第一种因为第二种容易让人误判——你以为是代码逻辑问题其实只是断点没挂上。还有一种情况是断点确实挂上了但那个方法压根没被执行到。这时候先别怀疑调试器去看一眼日志确认请求有没有走到你预期的分支。远程调试最大的价值是帮你看到以为走到了其实没走到这类认知偏差。4.4 调试卡顿、超时与性能影响调试端口开着但没命中断点的时候性能损耗其实很小日常使用基本感觉不到。真正影响大的是断点被频繁命中的场景如果断点打在一个每秒执行几百次的方法上每次命中都会挂起线程、和调试器通信、等待你点继续应用的响应时间会立刻飙上去。所以我在实际操作中总结了几条经验。第一优先在方法入口而不是循环体里打断点。第二善用条件断点比如只对特定订单号或者特定用户 ID 生效这样能过滤掉绝大部分无关请求。第三善用异常断点直接定位到抛异常的那一刻比在调用链上层层单步要快得多。另外如果在调试期间观察到操作明显变慢、甚至请求超时先不要怀疑是应用本身的性能问题有可能只是断点在拖后腿。把调试断开再测一次做个对照能省掉一大堆无效分析。4.5 调试结束后的收尾清理清单调试完不收尾是很多事故的起点。我自己固定执行这么几步在 IDEA 里停止 Debug 会话确认控制台里出现断开连接的信息。把setenv.sh里的调试参数注释掉或者把JPDA_ADDRESS那行删除。重启被调试的应用确认启动日志里不再出现Listening for transport dt_socket。从开发机再测一次端口连通性确认已经连不上。如果调试期间为了排障临时改过日志级别或者开关一并改回去。这五步加起来不超过五分钟但能避免这个端口是不是还开着这种长期悬而未决的风险。5. 进阶场景容器化与多实例调试现在越来越多的项目跑在容器里Tomcat 只是镜像里的一个进程配置方式的思路要跟着变一下。5.1 Docker 里的 Tomcat 怎么暴露调试口容器环境下有两件事要同时做对容器内的 JVM 要监听在所有网卡上容器本身要把端口映射出来。给镜像加环境变量FROM tomcat:9.0-jdk17 ENV CATALINA_OPTS-agentlib:jdwptransportdt_socket,servery,suspendn,address*:5005运行时映射端口docker run -d --name my-tomcat \ -p 8080:8080 \ -p 5005:5005 \ my-tomcat-image注意这里的两个关键点容器内的address必须写*:5005因为容器里的回环地址和宿主机不是一回事-p 5005:5005这一条不能少否则容器内监听了外面也连不上。如果是用编排工具部署的思路一样只是把端口映射写在编排文件里并且要注意只在需要的调试阶段开放用完就撤掉。注意容器调试端口一旦映射到宿主机就等于把整个容器进程的调试能力暴露在了宿主机所在的网络上。在共享环境里开这个口子之前务必确认访问边界。5.2 一个 IDEA 同时挂多个服务微服务架构下一次排查往往要同时看两三个服务的日志和状态。做法很简单给每个服务的 JDWP 端口错开比如 5005、5006、5007然后在 IDEA 里建三个 Remote JVM Debug 配置分别指向不同的 Host 和 Port。这里有个小技巧如果几个服务在同一台机器上Host 可以填同一个如果在不同机器上就按实际 IP 分别填。启动的时候可以选择同时启动多个 Debug 会话IDEA 会把它们的控制台分标签页显示看日志的时候来回切很方便。命名上建议带上服务名和环境标识比如payment-service-dev-5006。当你手上有七八个配置的时候这一条能救你一命。5.3 没有源码时的查看方式有时候你需要调试的是别人的包或者某个依赖库里的逻辑。这时候 IDEA 的反编译功能就派上用场了只要 class 文件在 classpath 上双击进去就能看到反编译出来的源码虽然变量名可能是var1、var2但流程逻辑是清楚的断点也能打。不过要提醒一点反编译出来的代码和真实源码会有出入尤其是泛型、lambda、语法糖这些地方。断点位置以反编译视图上的行号为准去打一般能停下来。如果停不下来就往上找一层调用方在自己能控制的代码边界上打。我更推荐的做法是优先在业务代码里打断点把框架内部的执行过程通过调用栈来观察。调用栈本身就是一份现成的执行路径记录很多时候看栈就够了不需要真的单步进框架。5.4 与热部署配合的边界不少人习惯开着热部署改了代码立刻生效调试的时候也顺手。这里要区分清楚两件事热部署解决的是代码改动怎么生效远程调试解决的是运行时状态怎么看。两者可以一起用但要注意热部署之后类被重新加载断点需要重新确认一遍思路和 4.3 节说的一样。我的实际用法是分开需要快速试改代码的时候用热部署需要认真看运行状态的时候关掉热部署、老老实实重启把断点重新挂一遍。混在一起用最后你很难判断某个奇怪现象到底是代码逻辑问题还是热部署把类搞乱了。还有一点某些字节码增强的框架在热部署之后行为可能不一致本来正常的逻辑突然报类型转换错误这种情况先重启一次再复现八成就能排除。最后分享一个我自己用下来最顺手的习惯每次排查完一个问题把当时用的调试参数、端口、IDEA 配置名、遇到的坑随手记一条到项目笔记里。下次再遇到类似问题翻笔记比重新查资料快得多。远程调试这件事配置本身十分钟就能学会真正花时间的是那些边角情况而边角情况基本都来自经验积累。
返回列表