
1. 调试思路先纠正别再从第一行开始按 F8我在带团队做 Code Review 的时候经常看到有人遇到 bug 的第一反应是“把断点打在 main 方法第一行然后一路 F8 往下按”。这种调试方式不能说没用但效率实在太低。项目小的时候还能靠肉眼跟完整个流程项目一旦上了规模一个请求从 Controller 进来经过 Service、Mapper、MQ、第三方接口再用这个方法去追光让程序走到“案发现场”就要花掉半天。真正高效的调试思路是让代码在“你觉得会出问题的位置”停下来然后通过栈帧、变量、表达式去验证你的怀疑而不是像读小说一样从头到尾把代码翻一遍。调试的本质是“制造可控停顿”不是“逐行观赏程序运行”。1.1 打断点之前先想清楚三件事很多人在 Debug 之前没有做过任何预判断点打在哪里全凭直觉。我的习惯是动手打断点前先问自己三个问题哪个模块最可疑是入参解析、业务判断还是数据落库什么时候会触发这段代码是第一次调用、循环第十次还是某个特殊数据进来之后我希望它在什么条件下停下来比如order.getStatus() 3还是list.size() 0的时候这三个问题想清楚断点位置基本就浮出来了。比如用户反馈“上传一个 2GB 的文件界面卡死了”你大概率不用在文件读取的第一行断点而应该直接去读文件分片循环、内存分配、异步任务提交这几个可疑位置。带着预判去调试才能避免在无关代码上浪费时间。1.2 几个天天在用的“导航型操作”除了最基础的 Resume ProgramF9和 Stop真正能提高日常调试效率的是这几个操作Run to Cursor光标定位运行断点停在 A你怀疑 B 行有问题不想再新起一个断点直接把光标放在 B 行按 AltF9程序会继续执行到光标所在行并停住。Show Execution Point回到当前行在 Debugger 窗口里翻看变量、堆栈时不小心把界面拖乱了点一下这个按钮可以立刻回到当前暂停的代码行。Force Step Into强制进入普通 Step Into 遇到某些三方库的方法不会进入或者进了 JDK 底层却不想慢慢跟用 Force Step IntoAltShiftF7可以强制进入任意方法包括很多被 IDEA 默认过滤掉的类。Step Out跳出当前方法跟到一个工具方法内部发现里面逻辑没问题直接 ShiftF8 跳出回到调用方继续看。这几个操作我建议直接记住快捷键。Debug 时鼠标来回点工具栏会打断你思考的连贯性。尤其是 Show Execution Point你会发现它在多方法嵌套、什么时候丢失当前行时特别能救命。1.3 栈帧里到底在看什么程序停在断点后不要急着往下走。先看 Debugger 窗口里的 Frames调用栈。调用栈能告诉你“当前方法是谁调过来的”“调用链上有哪些中间层”。很多隐蔽 bug 不是你当前这行写错了而是传入的参数从源头就不对。我调试时有个固定动作先把调用栈从上到下扫一遍找到第一层业务入口比对入参在每一层的变换情况。如果中间层有个方法把参数值改了断点当前位置的变量看起来就很奇怪。这时候你再顺着 Frames 往回点逐层查看方法参数值就能快速画出“数据是怎么变成这样的”。这比盲目 F8 更接近问题的真相。2. 断点类型拆解行断点只是开始剩下三种更值钱IDEA 里最常见的断点是行断点在行号右侧点一下程序运行到这一行就会暂停。但实际开发中真正烦人的问题往往和“方法入口”“字段被篡改”“异常被吞”有关这三种场景分别对应方法断点、字段断点、异常断点。断点类型添加位置典型场景说明行断点任意可执行代码行看某个具体位置的变量值最常用方法断点方法声明行想知道方法被调用、参数是什么性能有额外开销字段断点字段声明行成员变量字段值被莫名修改每次读写都会触发异常断点Debugger 面板配置异常被捕获后还想定位抛出点很强大但需要注意过滤2.1 方法断点不侵入方法内部先看谁在调它方法断点不是让你在方法第一行打行断点而是在方法名那一行直接打断点。IDEA 会把方法断点标记成一个特殊的图标。它的作用是只要方法被调用就会中断你可以直接看到入参再决定要不要进入方法体。有个很典型的场景一个public void updateStock(Stock stock)方法被好几个地方调用每次调用后库存都对不上。你在方法内部打行断点只能看到“进来的时候值是多少”但看不到“是谁把它调进来的”。如果改成在方法名上打方法断点IDEA 会帮你在方法进入和退出时都有机会暂停你可以看调用栈找到真正的调用方。不过方法断点有个坑它会让方法调用变得很慢尤其是高并发、高频率调用的方法。所以它适合“低频方法”或“需要快速定位调用方”的场景排查完记得及时删除。2.2 字段断点抓住那个偷改数据的人字段断点是在成员变量的声明行上打的断点。它的神奇之处在于字段被读取或者被修改的时候都会触发暂停。你可以设置为只在“字段值发生变化”时暂停这样一旦有代码修改了这个字段就能立刻定位到修改点。举一个我印象很深的案例一个订单对象里有个status字段明明代码里没有直接调setStatus()但界面上显示的状态老是变。用字段断点设置status字段“字段访问修改”都断然后重新触发一次业务程序直接停在某个第三方工具类里发现是反射调用Field.set()把值改了。如果不是字段断点仅靠业务代码走查根本发现不了这个隐藏修改点。字段断点也有限制如果你需要捕捉非常高频的字段修改程序会一直暂停体验会很差。建议配合条件表达式比如this.status ! oldStatus之类的条件再停。2.3 异常断点让“被吞掉的异常”现出原形Java 里常见的坑是异常被 catch 住后只打了一行日志甚至 catch 后没做任何输出。你在业务代码里打断点根本等不到异常因为异常已经被人为拦截了。这时候用Exception Breakpoints就很合适。在 Debugger 窗口打开断点管理CtrlShiftF8点击加号选择“Java Exception Breakpoints”填入你怀疑的异常类型比如NullPointerException。然后程序运行过程中只要这个异常被抛出无论有没有被 catchIDEA 都会在抛出位置暂停。我第一次用这个功能解决的是一个线上偶发的空指针。日志里只有一句log.error(处理失败)没有任何堆栈。我在 IDEA 里加了 NPE 异常断点运行测试用例程序直接停在了真正为 null 的那一行前后版本比对后发现是上游订单状态值映射漏了一个枚举。如果没有异常断点这种问题很难快速锁定。异常断点的缺点是范围太大会乱停比如 JDK 内部也经常抛出一些可预期异常。这时候可以在异常断点上设置过滤条件或者打开“捕获异常时也停”/“只在未捕获时停”的开关控制暂停时机。2.4 断点分组与批量管理项目大了之后断点会越加越多最后你会发现自己根本分不清哪些是旧的、哪些是当前排查需要的。IDEA 的 Breakpoints 窗口支持给断点分组、重命名还能一键静音Mute Breakpoints。我的一般做法是给不同业务模块的断点分到不同组比如“订单问题”“登录问题”。临时验证用的断点用完之后立刻删掉。不需要暂停但想保留的断点先取消勾选 Enable留给下一次排查。尤其是“Mute Breakpoints”这个功能有时候你不想真的执行到断点但又不想一个个删直接右键断点图标临时禁用全部断点。程序跑完了再恢复特别省事。3. 条件断点、日志断点和命中次数三个“精准打击”的实战场行断点虽好用但总不能每次遇到列表循环都要在循环体里停几百次。IDEA 的断点本身支持非常丰富的属性配置右键断点就能设置条件、日志、命中次数等。这一节聊的就是让断点“长眼睛”的方法。3.1 条件断点的正确写法与坑点打断点后右键断点红点或 CtrlShiftF8 进入断点管理在 Condition 栏输入一个 Java 布尔表达式。例如// 循环处理订单时只停在订单编号包含特殊前缀的订单上 order.getOrderNo().startsWith(RUSH) order.getAmount() 1000条件断点会在每次执行到这一行时先计算条件条件为 true 才暂停否则继续放行。这样假设一个列表有 10 万条数据你只需要在当前第 87 条停下查看就不用每一条数据都人工判断一次。写条件断点有几个容易踩的坑别在条件里调用重量级方法。每次到这里都要执行一次如果条件里写了个远程 RPC 调用程序会卡到怀疑人生。注意局部变量是否在当前作用域内。Lambda 表达式里某些循环变量访问方式和普通 for 循环不一样条件里看不到对应的变量时可以改成用外部类的字段来判断。条件表达式本身抛异常时IDEA 会按“条件不成立”处理程序不会暂停。所以条件里尽量用 null-safe 的写法比如ok.equals(obj.getStatus())而不是obj.getStatus().equals(ok)。我平时定位“某一个特定数据引发的 bug”时最常用的动作就是在循环体断点加一个id xxx的条件。这比一遍遍改断点位置高效太多。3.2 日志断点让线上代码悄悄说话日志断点在 IDEA 里的学名叫“Breakpoint properties”中的 Log message / Log evaluated expression。它和普通断点的区别是命中断点时不会暂停程序而是把指定信息输出到控制台然后继续运行。场景是这样的一个批量任务要处理 10 万个文件你怀疑某个文件中某个字段格式导致后续计算全错。如果用行断点程序会在第一个文件就暂停你需要手动点“继续”十万次这显然不现实。改成日志断点以后你可以在断点处设置表达式把当前文件路径、字段值打印出来程序全程不暂停最后只需要看控制台日志就能分析哪些文件有异常。日志断点的配置也很简单右键断点勾选 “Log message to console”。在 message 里写一句话用{}占位符引用变量。如果想要详细堆栈就再加一个 “Log stacktrace to console”。我特别喜欢把日志断点和条件断点结合起来用先设置一个条件只有满足条件才打印日志。这样连“大量重复日志”的问题都避开了。调试完毕后日志断点保留不保留看情况如果控制台日志已经够用直接删除断点也不心疼因为它本来就不影响流程。3.3 命中次数的场景只看第 N 次Pass count 这个配置常用于“问题只在前 N 次不会出现后面才出现”的循环或递归场景。比如一个分页查询接口第 3 页返回的数据异常但前两页都正常。这时候断点加条件page 3就可以了。但如果是在一个没有明确条件的递归逻辑里你只想在递归深度到 100 的时候停下来看看那 Pass count 就派上用场了。设置 Pass count 后断点会在第 N 次命中时才真正暂停。这里注意IDEA 里有些版本把 Pass count 写为“会跳过前几次”不同版本的文案略有差异总之“命中 N 次才真正触发”这个概念能对上。3.4 断点条件计算对性能的影响条件断点好用但代价也不小。程序每执行到这一行都需要额外做一次表达式计算。如果一个条件表达式本身很慢比如遍历一个大集合、调用数据库、做字符串匹配复杂正则那整体性能会明显下降。我实测过一个大集合的循环中条件里写了个list.contains()结果 100 万条数据整整跑了十几分钟。后来改成在循环外面用一个 Set 判断瞬间就快了。这类性能问题不是 Debugger 的锅而是条件表达式写得不够高效。调试场景下也要养成“用完就清理”的习惯临时条件断点改过的代码、加过的表达式排查完尽量还原否则下次再跑性能问题就会重现。4. 多线程与异步代码调试线程视图、挂起策略与并发套路多线程代码是调试里的老大难。单线程下断点停住整个世界都静止多线程下断点停住往往只是一个线程停了其他线程还在继续跑状态互相影响很容易把问题看乱。IDEA 的 Debugger 对多线程调试提供了不少支持但很多人没有认真用过。4.1 先分清 Threads 与 FramesIDEA 的 Debugger 窗口顶部一般有两个下拉/列表区域。左侧或者上方的 Threads 列表显示当前所有线程右侧是当前选中线程的调用栈 Frames。很多初学者会忽略 Threads 列表只盯着当前 Frames 看。但多线程问题恰恰需要你不断切换线程视角先看哪个线程处于RUNNABLE、哪个线程处于BLOCKED、哪个线程处于WAITING。选中一个线程后下面 Frames 会切换成它自己的调用栈。如果多个线程都停在同一个锁上你能看到它们在等谁释放锁。有一次我排查一个死锁问题两个线程互相持有对方的锁。我就是在 Threads 列表里切换查看两个线程的栈帧分别看到各自停在synchronized方法上又检查了 Monitor 对象信息很快锁定了加锁顺序不一致的问题。4.2 Suspend All 与 Suspend Thread 的区别断点属性里有一项是 Suspend policy分为两个选项Suspend All所有线程都暂停整个 JVM 停在断点处。Suspend Thread只有命中断点的当前线程暂停其他线程继续跑。默认是 Suspend All适合大多数场景因为暂停整个应用状态比较稳定方便查看全局数据。但在调试“某几个线程在竞争其他线程只是背景噪声”的场景时Suspend Thread 会更好用。举个例子一个线程池跑了 20 个任务其中有一个任务处理异常。如果你用 Suspend All断点一命中20 个线程全停了。你其实只关心出问题那个线程其他线程停不停对你排查没有帮助反而会掩盖竞争条件。这时候改成 Suspend Thread只把那个任务线程停住其他线程继续执行你能更贴近真实运行状态。Suspend Thread 也让调试变得更复杂因为其他线程还在跑你查看的共享变量可能一直在变。所以我的建议是默认 Suspend All只有明确发现“其他线程继续跑才会复现 bug”时再改 Suspend Thread。4.3 用条件断点按线程过滤如果你只想在某一个特定线程上停下来最粗暴的办法是在断点条件里写线程名Thread.currentThread().getName().contains(order-task)这样无论多少个线程执行到这一行都只有线程名带 “order-task” 的那个会真正暂停。这个技巧在处理线程池、异步任务、定时任务时非常实用。还有一个注意点Spring 的 Async、定时器线程池默认都会有命名的线程前缀比如SimpleAsyncTaskExecutor-1、scheduled-1等。你可以先停一次在条件里输出Thread.currentThread().getName()确认线程名再补上精确过滤。4.4 多线程断点导致的“假死”和虚假等待很多人调试并发代码时会发现一旦断点命中某个线程程序就卡住不动了点击 Resume 也没反应。这往往是因为被暂停的线程持有锁而其他线程正在等待这个锁你恢复了主线程发现下游还得继续走但停住的那个线程还没释放锁导致整个流程推进不下去。遇到这种情况先别慌。先看当前断点命中的线程是不是锁的持有者再看等待方线程卡在哪里。一个常用的变通方案是在断点处用 Evaluate Expression 主动执行“解锁”或者修改状态或者干脆先把断点禁掉让线程跑完。否则你会在“死锁调试”里越陷越深。排查并发问题的核心是“看线程状态、看锁对象、看调用栈”IDEA 的多线程视图把这些信息都摊开了剩下的就是你的逻辑判断。5. 集合与 Stream 调试用 Trace Current Stream Chain 看懂每一步Java 8 之后集合和 Stream 的链式调用越来越常见但链式表达式调试起来特别别扭。你想知道一个map之后的数据变成什么样通常的做法是在 Stream 中间某一行打断点然而 Lambda 里没有明显的行号一个链式表达式往往写在一两行内断点一停就是整条链式调用的中间某步。IDEA 专门为这种场景提供了 Stream 调试能力。5.1 集合里到底装了什么展开与自定义视图程序停在断点处Variables 面板会显示当前集合对象。List、Map、Set 都可以展开看到内部元素。对于 List默认显示的是元素列表和长度对于 Map显示的是键值对。这一块很多人都会用但很少有人会去点集合对象左侧的“View”下拉或者邮件选择“Evaluate Expression”。实际工作中我经常遇到集合对象已经展开但元素太多、找到目标元素很费劲的情况。这时候我会直接在集合对象上右键使用 Evaluate Expression写一段临时表达式来过滤// 在 Evaluate Expression 中执行找出某个用户的订单 orders.stream().filter(o - o.getUserId().equals(u_10086)).collect(Collectors.toList())调试时用 Evaluate Expression 跑一段临时逻辑不会影响程序本身却能帮你在巨大集合中快速缩小范围。5.2 Trace Current Stream Chain把每一步都拉出来看这是 IDEA 2018 之后加入的功能对我来说算是“救命级”的调试工具。当断点停在 Stream 链式调用上时Debugger 面板会多出一个按钮通常叫Trace Current Stream Chain图标看起来像一串小圆点加箭头。点击后IDEA 会打开一个新的 Stream 调试窗口把整条 Stream 的每个中间操作拆成列展示每列显示当前步骤的元素内容和变化过程。比如下面这段代码ListString result orderList.stream() .filter(o - o.getAmount() 100) .map(Order::getOrderNo) .distinct() .collect(Collectors.toList());点开 Trace Current Stream Chain 后你能看到 filter 前有多少订单、filter 后留下来多少、map 之后变成哪些订单号、distinct 之后又剩多少。这样一来如果 final result 不对你一眼就能看出是 filter 条件过严还是 map 转换时字段取错。我之前遇到过一个问题用户下单数量统计总比实际少几条。业务代码里对订单流做了三次 filter我一行行执行太麻烦用 Trace Current Stream Chain 直接看到第二次 filter 把一批“订单金额为 0 但确实下单成功”的记录全过滤掉了。如果不用这个功能我要么拆代码加日志要么手动算一遍中间值排查成本高得多。5.3 用 Evaluate Expression 拆解长 LambdaStream 链式表达式如果很长比如一个流水线套了三层 map、两层 filter中间还引用了一个外部工具类光靠读代码很难看出每一步的真实输入输出。我通常会在断点处用 Evaluate Expression 分步执行整条流水线的一个片段。例如先执行orderList.stream().filter(o - o.getAmount() 100).collect(Collectors.toList())查看暂时的过滤结果再继续在上一步结果上追加 map 操作。这会非常直观地展示 Stream 的执行路径。另外Evaluate Expression 还有一个很实用的场景临时调用某个 getter 方法。比如你看到变量user上有 id、name 字段但你不知道user.getAddress()是否为空直接在 Evaluate Expression 里输入user.getAddress()回车就能看到结果不用再去浏览器或测试工具里拼接口。5.4 不重启就调整 Lambda 里的局部变量在 Lambda 表达式内打断点有时候会遇到一个问题lambda 内部引用的外部局部变量会被拷贝成一个新变量你在 Variables 面板里修改外部变量的值可能不会影响 lambda 里的那份拷贝。这种情况下直接在断点处的 Evaluate Expression 里改 lambda 内可见的变量效果更可控。比如循环里有个map操作把字符串转成大写你想看看如果把输入改成“TEST”会发生什么。断点在 lambda 内部停住后直接在 Evaluate Expression 里把参数变量的值设置成TEST再继续往下走就能验证你的猜想。但这种修改只对当前执行帧有效别指望它能回写外部集合。6. 不重启也能改代码Drop Frame、Set Value、Force Return 与热加载调试的终极效率是在不停应用、不重新部署的情况下验证你的修改是否有效。IDEA 里 Drop Frame、Set Value、Force Return 这几个操作组合起来几乎可以在运行时“实时改剧本”前提是你得理解它们的边界。6.1 Drop Frame 到底干了什么Drop Frame 在 Debugger 工具栏上点击后当前栈帧会“弹出”程序会重新回到该方法的调用处再次进入这个方法。很多人以为它是“回滚代码”其实不完全是。它能让当前方法重新执行但不会重置局部变量如果你的方法内部在进入之前修改了某个对象的字段重新进入时会保留这个修改。它只能往前放弃当前栈帧不能反向跳过已经执行完的调用栈。它不能跨过递归、不能跨过 native 方法、不能跨过某些第三方代理对象。我最常拿它来反复走同一段逻辑。比如有一个方法想把订单状态从“待支付”改成“已支付”但每次改完又希望重新走一遍同一段逻辑验证不同分支用 Drop Frame 就能避免重新发起一次完整的接口调用。6.2 Set Value在断点处“篡改”数据在 Variables 面板里选中一个变量按 F2 或者右键 Set Value可以直接修改它的值。这个功能在调试时非常实用。举个例子你在循环里想测试order.getStatus() 3和order.getStatus() 5两种分支。如果每次都改代码里的入参然后重启效率太低。直接在断点处把status字段的值改成 5继续执行就能进入另一个分支验证逻辑。我在真实项目里还会用它来绕过一些不好构造的入参。比如第三方接口返回的对象在本地很难模拟一个带 100 个字段的响应这时候断点停在解析处直接 Set Value 把关键字段改成期望值后面流程正常执行即可。注意Set Value 修改的是 JVM 内存中的对象引用或值不是修改源码。如果代码逻辑后续又对这个变量赋值了修改会被覆盖。6.3 Force Return 和 Throw Exception这两个操作都在右键当前栈帧的菜单里。Force Return把当前方法强制返回一个指定值方法后面的代码不会执行。Throw Exception在当前位置抛出一个指定异常用来模拟异常路径。正常开发中这两个操作主要用于“测试异常分支”和“跳过耗时操作”。比如调用一个第三方查询接口本地环境连不上你可以 Force Return 一个 mock 结果然后继续往下调试业务逻辑。或者你想看 catch 块里的告警日志是否正确直接在 try 块里 Throw Exception程序就会进入你期望的异常处理路径。6.4 热加载的范围与限制Debug 模式下IDEA 支持对已运行的 JVM 做热加载。当你修改了方法体、字段值、变量名等“不改变结构”的内容点击 Build Project 或使用 JBR 的热替换新代码一般能直接生效。我之前调一个工具类的排序规则改了 compare 方法里的一个条件不用重启 Debug 会话运行中的程序下一次调用就是新逻辑。但热加载有硬性边界新增方法、新增类、修改方法签名、修改继承关系基本都不能热加载IDEA 会提示你需要重启。修改 Lambda 表达式时情况比较复杂有的版本支持有的版本会提示“类结构已变化需要重启”。修改注解、泛型、静态变量类型也有可能不生效。所以我的经验是Debug 过程中小改直接用热加载大改还是重启稳妥别为了省那几十秒浪费更多排查时间。7. 断点失效、卡顿、找不到源码——常见翻车现场逐个排雷调试工具本身也会出问题。最让人血压升高的事情莫过于断点在行号上明明打了红点程序也执行到了这一行但就是不暂停。这里我把实际中常见的“断点翻车”梳理一遍。7.1 断点不命中的几类原因断点被 Mute静音了。如果断点图标上有一条斜线右键检查一下是不是点了 Mute Breakpoints。这个问题最常见最容易忽略。代码没有重新编译。IDEA 里改了代码但 Debug 运行时用的还是旧的 class。尤其是 Maven/Gradle 多模块项目某个子模块的源码改了如果没有触发编译断点就落在旧 class 上。我一般会先 Build Project 再重新 Debug。没有走 Debug 模式。听起来像废话但真的有人用 Run 模式启动程序然后再从 Debugger 窗口加断点发现不生效。断点打在接口上或抽象方法上。这类位置没有实际的执行代码需要打在实现类或具体方法上。断点打在 Spring 代理类上。项目中大量使用 Transactional、Async 时实际执行的是代理对象里的逻辑IDEA 有时无法将断点精确映射到目标行。可以尝试在事务方法实现类上加断点或者关闭部分代理配置。7.2 调试时程序卡死或特别慢条件断点太频繁。每次执行到断点都计算一次条件表达式又很重程序自然慢。方法断点太多。方法断点开销远高于行断点尽量不要在循环内的方法上打方法断点。日志断点打印了海量日志。如果条件范围太宽控制台的输出本身就能把 IDE 拖垮。调试器变量计算副作用。IDEA 的 Variables 面板默认会显示变量值某些对象在显示时会调用 toString()。如果 toString() 实现里有网络请求或复杂计算调试就会非常慢。遇到这种情况可以在断点属性中关闭自动预览或者把该类的 toString() 临时注释掉。7.3 变量看不到、源码找不到断点停了但 Variables 面板里只有this看不到局部变量。有一种可能是当前停在类加载阶段或者 JIT 优化过的代码上IDEA 没法采样局部变量。还有一种可能是停在了反编译出来的第三方类上源码缺失。遇到源码找不到点击“Download Sources”下载对应 jar 的源码。遇到局部变量不显示可以先用 Evaluate Expression 手动输入变量名看能不能取到值。如果 Evaluate 也没有那多半是代码被 JIT 编译优化了可以尝试在 Debug 配置里关闭 JIT 相关优化其实 IDEA 默认已经做了很多处理个别江湖偏方是调整 -Xint 参数不推荐在生产环境干这种事。7.4 我个人的一个习惯每轮排查只保留“怀疑链条”上的断点最后分享一个我自己的调试习惯每轮排查只保留“怀疑链条”上的断点。不要一口气打 20 个断点程序停来停去最后自己都不知道看到的是哪一层的数据。正确做法是先打一个入口断点确认数据进入时是对的再往下走一步在下一个分叉点加断点逐步缩小范围。每验证完一个环节就把旧断点删掉。这样调试链路清晰也不容易出现“断点太多互相干扰”的问题。Debug 这种东西技巧学再多不练永远没感觉。你平时可以先拿自己项目里一个慢接口练手从“去掉 verbose 日志”开始把断点、条件、Evaluate Expression 这几个基本功用熟。等哪天遇到一个莫名其妙的并发问题你会发现下意识就打开了线程视图加了线程条件断点十分钟内就把问题圈出来了。到那个时候Debug 对你来说就不再是“试错”而是一种顺手的习惯。