前言
本文如题目,基于 IDEA,基于 Java,本来想用 Dart 来着,寻思了一下还是用更多人擅长的来讲吧
调试是个很牛逼的东西,因为你不能保证你能看懂代码抛出的每一个异常是什么意思,代码的每次编译错误都是什么问题,服务抛出的各种问题你都能找到位置
所以说,调试在越复杂的项目里面越实用。
貌似,jetbrain 家的IDE 都可以沿用类似的这一套调试流程和方法
进行调试之前,我们需要了解什么
还是先说一下 IDEA 中如何进行调试,我确实没特意看过这个内容,全是我自己摸索出来的,也顺便总结一下
首先,调试的情况分为如下类别
- 本地调试:最常见的调试方式,针对当前开发环境中运行的 Java 程序(如本地启动的 Spring Boot 应用、普通 Javamain 方法等)。代码和程序均在本地机器运行,调试器直接连接本地进程。
- 远程调试:针对运行在远程服务器(或其他机器)上的 Java 程序进行调试,本地 IDEA 通过网络连接远程进程。代码在本地,程序在远程运行,通过调试端口建立通信。
- 测试调试:针对 JUnit、TestNG 等单元测试用例的调试,仅运行指定的测试方法,断点可设在测试代码或被测试代码中。
- 条件断点调试:对断点设置触发条件,仅当满足条件时才暂停程序,避免无意义的中断。就是右键断点图标,勾选
Condition,输入 Java 表达式 - 异常断点调试:当程序抛出指定异常时自动暂停,无需在代码中手动设置断点。一般是排查未捕获的异常,定位异常发生的具体位置。在
Breakpoints窗口(Ctrl+Shift+F8)点击+,选择Java Exception Breakpoints,指定异常类型(如java.lang.NullPointerException)。 - 分布式调试:针对分布式系统(如微服务架构)的调试,涉及多个服务实例之间的调用跟踪。一般是用工具
其实可以去看官方文档,肯定比我全
http://www.jetbrains.com/help/idea/debugging-code.html
了解IDEA调试中各个页面的作用
基础通用部分
先看一下页面,随着idea版本的更新Debug模式的图标设计虽有微调整,但是差不多
绿色三角大家都知道是运行,绿色虫子大家都知道是调试,最左边那个 “Toggle Smart Step Into”(智能步入切换) 功能按钮,差不多它的作用就是调试时使用 “智能步入”,遇到方法调用,会智能判断,优先进入用户代码而非库代码,让调试更高效,避免频繁进入不想看的框架、库等代码里,精准控制调试步骤 。
建议每个人把 在断点上使应用程序获得焦点 选项打开
如果你希望 调试 工具窗口在命中断点之前保持隐藏,请在相应的 运行/调试配置 中取消选中 启动时打开运行/调试工具窗口 复选框。
如果你的IDEA底部没有显示工具栏或状态栏,可以在View里打开,显示出工具栏会方便我们使用。访问请求到达第一个断点后,会自动激活Debug窗口。如果没有自动激活,可以去设置里设置。
以我的一个微服务项目为例子,微服务的调试界面就比较复杂了,介绍一下各个窗口的作用
位于调试窗口的上方,包含了一系列用于控制调试流程的按钮,把鼠标光标放到上面就能查看,从左到右通常有:
重新以 Debug 形式启动
停止运行
继续:快捷键 F9,程序会继续执行,直到遇到下一个断点或程序结束。在暂停状态下恢复程序的执行。
暂停:快捷键忘了,程序会被暂停,用于查看情况
步过:快捷键 F8,会执行当前行代码,如果当前行是方法调用,会直接执行完整个方法,不会进入方法内部。常用于快速跳过已知逻辑正确的方法。
- 在调试程序时,步过是一种调试技术,它允许你逐行执行代码,而不会进入当前行中的函数或方法。当你在调试过程中遇到一个函数或方法调用时,使用步过命令可以直接跳过该函数或方法的执行,继续执行下一行代码。
步入:快捷键 F7,如果当前执行点在方法调用处,会进入被调用的方法内部,逐行执行方法内的代码。适用于深入查看方法内部的执行逻辑。
- 步入是另一种调试技术,它允许你进入当前行中的函数或方法,并逐行执行其中的代码。当你在调试过程中遇到一个函数或方法调用时,使用步入命令可以进入该函数或方法内部,逐行执行其中的代码。
步出:快捷键 Shift+F8,会从当前方法返回到调用它的方法,继续执行调用方法中的后续代码。当你在方法内部调试完,想回到调用处继续执行时使用。
- 步出是一种调试技术,它允许你从当前正在执行的函数或方法中退出,并返回到调用该函数或方法的地方。当你在调试过程中进入了一个函数或方法内部,但你已经完成了对该函数或方法的调试,可以使用步出命令来退出该函数或方法,并返回到调用它的地方。
查看断点:快捷键 Ctrl+Shift+F8,会打开断点管理窗口,你可以在其中查看、启用、禁用和删除所有断点,还能设置断点的条件等。
忽略端点:没啥好说的,就是不要这个断点了
更多:如下内容,自己了解一下,不多说
- 说一下其中智能步入是什么意思,就是如果一行中有多个方法调用,IntelliJ IDEA 会询问您要进入哪个方法。 可以配置 智能步入 使其在每次同一行有多个方法调用时自动使用
- 强制步过和强制单步执行用到的很少,但是还是说一下
- 强制步过在 Windows 和 Linux
系统中一般是
Ctrl + Shift + F8;当执行到方法调用语句时,使用 “强制步过”,调试器会将被调用的方法当作一个整体执行完毕,然后停留在方法调用语句的下一行,不会进入到被调用方法的内部。即便被调用方法中存在断点,调试器也不会在那些断点处暂停。 - 当你对某个方法的内部逻辑已经很清楚,不需要深入查看其具体执行过程,只关心方法调用后的结果和程序后续执行情况时,就可以使用 “强制步过”。
- 强制单步执行在 Windows 和 Linux
系统中是
Alt + Shift + F7;“强制单步执行” 会深入到任何方法内部,包括 Java 标准库方法、第三方框架的方法以及 Lambda 表达式等。不管被调用的方法是用户自定义的还是系统提供的,调试器都会进入该方法内部,逐行执行代码,并且会在遇到的每个断点处暂停。 - 当你想要深入了解某个方法的具体执行逻辑,查看其内部变量的变化情况,排查方法内部可能存在的问题时,就需要使用 “强制单步执行”。一般看源码的时候用到的多
- 强制步过在 Windows 和 Linux
系统中一般是
来到线程和变量面板
可以看到这里会展示一些你的调试的信息,包括变量,值,也就是说,如果你需要进行查看各个变量的执行情况和各个方法的执行情况,都会在这里进行显示
查看变量可以将Variables区中的变量拖到Watches中查看
可用的调试会话被分隔在 调试 工具窗口顶部的选项卡中。也就是这些
如果为特定的运行/调试配置 启用了服务工具窗口 ,那么当您调试这些配置时,调试工具窗口的整个视图将显示在服务工具窗口中。
这边其实还藏着一个选项卡页面,每个会话都会显示以下选项卡
帧(Frames)
- 作用:显示当前线程的调用堆栈(方法调用链)。
- 通俗理解:比如你的代码执行到方法 C,而方法 C 是被方法 B 调用的,方法 B 又是被方法 A 调用的,“帧” 就会按顺序列出 A→B→C 这样的调用关系。你可以点击切换不同的 “帧”,查看对应方法执行时的变量状态,相当于 “回退” 到上层方法查看当时的情况。
变量(Variables)
- 作用:展示当前暂停位置(断点处)可见的所有变量及其值。
- 特点:不仅能看,还能直接修改变量的值(比如把
count=0改成count=10),让程序继续执行时使用新值,方便快速测试不同场景,不用重新运行程序。
监视(Watches)
- 作用:自定义跟踪某些表达式或变量的变化。
- 用法:比如你关心
user.name这个值在整个调试过程中的变化,就可以把它添加到 “监视” 中,无论程序执行到哪里,都会持续显示这个值,不用每次在 “变量” 面板里找。默认和 “变量” 在同一个标签页,数量多的时候可以单独显示。
控制台(Console)
- 作用:显示程序运行时的输出内容。
- 不同场景
- 本地调试时:和正常运行程序的控制台一样,会显示
System.out或print输出的内容,同时额外显示调试相关的日志(比如 “命中断点” 的提示)。 - 附加到已运行的进程调试时:只会显示调试器自己的日志,不会显示原程序的输出(因为原程序输出可能在它自己的控制台里)。
- 本地调试时:和正常运行程序的控制台一样,会显示
线程(Threads)
- 作用:列出程序中所有正在运行的线程,包括它们的状态(运行中、阻塞、等待等)。
- 用途:可以切换到不同线程查看其调用堆栈(配合 “帧” 使用),适合调试多线程问题(比如死锁、线程安全问题)。还能导出线程快照(线程转储),用于分析线程状态。
内存(Memory)
- 作用:展示 JVM 堆内存中对象的信息,比如某个类有多少个实例、占用多少内存等。
- 用途:帮助分析内存使用情况,比如查找内存泄漏(某些对象明明不用了却没被回收,数量一直在增加)。
开销(Overhead)
- 作用:监控调试过程中各种功能消耗的系统资源(比如 CPU、内存)。
- 用途:如果调试时发现程序运行变慢,可能是某些调试功能(比如监视了太多表达式、内存分析等)导致的,通过 “开销” 面板可以找到资源消耗高的功能,关闭或优化它们来提高调试效率。
运行页面可以找到调试的大部分内容和快捷键
Spring相关调试部分
继续看 Bean 这个页面,通常是和 Spring 框架 集成相关的调试视图,主要用于在调试过程中查看、分析 Spring 容器中 Bean 的相关信息
- 关联 Spring 框架:如果你开发的是 Spring(含 Spring Boot 等)应用,Spring 容器会管理大量 Bean(如 Controller、Service、Repository 等组件)。调试时,借助这个视图能直观了解 Bean 的加载、配置和依赖关系 。
- 调试场景:比如排查 Bean 注入失败、配置未生效、依赖关系异常等问题时,可通过这里查看 Bean 实际的属性值、依赖关联,辅助定位问题。
其中图模式慎点,小心电脑内存瞬间爆炸
继续看运行情况这个页面
这个需要 IDEA 的 Profiler 功能(如 Async Profiler
),需要调试会话(Debug
Session)和性能分析会话同时开启。如果只是普通
Debug(只走断点调试,没启用 Profiler ),“运行状况”
就会不可用。需要启动调试时,选带 “Profiler” 的启动配置(如
Debug with Profiler ),而不是普通的
Debug。
这个部分的核心是 监控程序调试过程中的性能数据、执行耗时、方法调用热点 ,帮你排查性能问题、定位代码里的慢操作。老版本好像叫“性能分析
它一般会进行方法执行耗时统计,展示程序执行的 “热点方法”(被频繁调用或耗时久的方法 ),查看方法执行时的内存分配、CPU 占用,判断是 “计算密集型” 还是 “内存泄漏型” 问题。跟性能相关基本都在这看
现在他好像比 JVisualVM 好用,以前我们都用这个进行性能分析
继续看映射这个页面
这个页面主要就是展示
接口路径与处理方法的映射关系,左边路径列就是对应显示接口的访问路径,包含
HTTP 方法,右边方法列就是展示路径对应的 Java 方法,格式一般是
[控制器类]#[方法名] 。
路由可视化:清晰列出 Web 应用中,外部可访问的接口路径(如
/api/v1/iterations),以及背后对应的 Java 处理方法(如IterationController#queryIteration)。不管是自己开发的业务接口,还是 Actuator 这类监控端点(像/actuator/threaddump),都能直观看到 “路径 ↔︎ 方法” 的对应关系。调试辅助:调试时,若你想排查某个接口的逻辑,通过这里能快速定位到接口对应的 Controller 方法,直接找到代码入口;也能反过来,从代码里的 Controller 方法,对应到实际对外暴露的路径,验证路由配置是否正确。
继续看环境这个界面
这个页面是程序 “运行时环境参数的总览窗口”,把分散在配置文件、系统变量、环境变量里的参数,集中展示出来。无论你调试的是 Spring Boot、普通 Java 程序,还是其他框架项目,都能在这里快速查看、验证运行时生效的配置
一般在实际开发的过程中,都是在这看看配置是否生效,或者 profiles 的情况
非 Spring 项目在这里一般都是JVM 系统属性和环境变量,本质是统一管理程序运行时的 “环境上下文”,让你不用到处找配置、敲命令查参数,调试时更聚焦代码逻辑
IDEA调试技巧
如何查看变量
查看变量是我们再进行调试过程中,确认项目是否正确运行的前提,通过传递参数的情况来判断项目是否有问题,有什么问题,之后会怎么样
在 IDEA 中,参数所在行后面会显示当前变量的值
例如,我调试我项目的登录过程
发现在environment后面给出了其参数的值
或者在下面,线程和变量的控制台,也可以看到其变量及其值
光标悬停到参数上,显示当前变量信息。然后可以打开,这种很方便
在遍历 EnvironmentPostProcessor 并执行其
postProcessEnvironment
方法。其中出现了循环,说明ApplicationEnvironmentPreparedEvent
被重复发布,进而重复执行这段遍历逻辑。
发现是我项目中通过 spring.factories
或自动配置重复注册了同一个 EnvironmentPostProcessor
实现类,导致遍历列表中存在多个相同实例,反复执行。
这个部分就是一个比较好的详细查看变量的例子
在调试中,检查变量的目的不仅是检查变量值是否符合预期,而且当程序抛出异常(如
NullPointerException、IndexOutOfBoundsException)时,通过查看变量可快速定位原因。
而且观察变量在代码执行过程中的变化轨迹也很有用,是确保逻辑正确的标准。例如:跟踪用户输入数据从接收、校验到存储的全流程,可以确认每一步处理是否正确
打算法竞赛时候,也可以根据这个分析复杂对象状态,通过展开变量查看内部细节,判断是否存在数据遗漏、重复或错误赋值
如何计算表达式
在前面提到的计算表达式按钮,Evaluate Expression (Alt + F8) 。可以使用这个操作在调试过程中计算某个表达式的值,而不用再去打印信息。
继续我们的调试,我们发现程序在 SpringApplication.run()
方法的创建应用上下文(ApplicationContext)” 这步
众所周知,这一步是 Spring Boot
启动流程的关键步骤,ApplicationContext 是 Spring
容器的核心
那么这一步中并没有进入上下文初始化,此时 context
我们求值一下,发现为
null,所以这步是完全正常的,因为这行代码的作用就是创建
ApplicationContext 实例并赋值给
context。
那么结合变量查看部分,我们发现想要的查看变量直接就能被计算出来
而且这个表达式不仅可以是一般变量或参数,也可以是方法,当你的一行代码中调用了几个方法时,就可以通过这种方式查看查看某个方法的返回值。
我们的例子就是计算了 this.createApplicationContext();
这个方法在此时的环境上下文初始状态
表达式求值在这部分
如何回退断点
尤其是 spring boot 这种比较繁杂的项目的时候,经常因为“下一步”按太快,而导致跳过了想要深入分析的那段代码
在IDEA中就提供了一个帮助你回退代码的机会,但这个方法并不是万能的。
这个小的回转箭头就是 IDEA 的 Reset Frame 回退操作
在调试时,IDEA 的 “调用栈”(Call
Stack)面板会显示当前线程的执行路径(从方法调用的起点到当前断点的所有方法层级)。Reset Frame
功能允许你将执行位置回退到调用栈中某个上层方法的断点处,仿佛
“时光倒流” 到该方法执行的某个状态,从而重新调试这段代码。
为什么说回退不是万能的
先来看Reset Frame 能回退的场景
纯 Java 方法调用(无状态修改)
当方法仅进行局部变量运算、无副作用的参数传递或只读操作时,回退效果最理想。例如:
1
2
3
4public int add(int a, int b) {
int c = a + b; // 断点1
return c; // 断点2
}若在 “断点 2” 执行后回退到 “断点 1”,局部变量
c会恢复到计算前的状态,可重新执行加法逻辑。未修改外部状态的方法
若方法仅读取对象属性(不修改)、不操作全局变量 / 静态变量、不涉及 IO / 网络等外部资源,回退时能准确恢复到之前的状态。
再来看Reset Frame 不能回退的场景(局限性)
修改了对象状态或全局变量
若方法中修改了对象的属性、静态变量、集合内容等,回退仅能重置当前方法的局部变量和执行位置,但已修改的外部状态不会恢复。例如:
1
2
3
4
5
6
7public class UserService {
private User user = new User();
public void updateName(String name) {
user.setName(name); // 断点:修改对象状态
}
}若调用
updateName("test")后回退,user.name仍会保持 “test”(已被修改),回退后重新执行会再次叠加修改,导致状态混乱。涉及 IO、网络、数据库等外部操作
若方法执行了文件写入、数据库更新、发送网络请求等操作,这些 “副作用” 是不可逆的。例如:
1
2
3public void saveToDb() {
jdbcTemplate.update("INSERT INTO logs VALUES (?)", "test"); // 断点
}多线程环境
若当前线程修改了共享变量,其他线程可能已读取该变量并执行了后续逻辑。回退当前线程后,其他线程的状态无法同步回退,会导致线程间状态不一致,调试结果失真。
native 方法或 JNI 调用
对于调用底层 C/C++ 实现的
native方法(如System.currentTimeMillis()),回退无法重置其执行结果(如时间戳已被获取),因为这类方法的执行不受 Java 调试器控制。异常抛出后
若方法中抛出了异常且已被捕获或传播,回退到异常抛出前的栈帧时,异常状态可能无法完全清除,导致重新执行时逻辑异常。
那么为什么回退不是万能的,根本原因是:Java 是 “按值传递” 的语言,且调试器只能控制代码执行流程和局部变量状态,无法逆转 “副作用”。
回退本质是 “重置当前线程的调用栈和局部变量”,但对方法执行过程中产生的外部状态修改(对象属性、静态变量、IO 操作等)无能为力。
对于 Spring Boot 这类复杂框架,其内部大量涉及 Bean 状态修改、容器初始化、资源加载等操作,回退后很可能因状态不一致导致后续调试混乱(例如重复初始化 Bean 引发冲突)。
它的位置在这里
复杂场景谨慎使用,例如在我的例子中,在 Spring Boot
的核心流程(如ApplicationContext初始化、Bean
生命周期)中,若已涉及容器状态修改,回退可能导致调试状态异常,这时候一般重启调试更稳妥。
在关键代码行设置条件断点(如满足特定参数时暂停),避免因 “下一步” 太快跳过目标代码,从源头减少对回退功能的依赖。
如何进行debug中断
怎么中断正在debug的请求,也就是放弃此次http请求
有时候我们用IDEA进行debug,跑进来了,debug到某个断点或某一行,如果此时我们不想继续走下去(中断此次http请求,或者说中断此次debug),要怎么做?
为什么需要中断此次debug?
因为大部分请求不是 GET 的都可能涉及到状态的修改
这可能会导致因为调试导致数据库不干净了就,这是很不理想的调试,可能造成麻烦
此时需要一种方式:在不执行后续代码的前提下,直接终止当前请求,且尽可能避免已执行的状态修改(或减少影响)。
利用 drop frame 回到最顶层(一般是controller方法),右键点击栈帧选择Froce Reutrn (也就是强制返回)即可(方法如果有返回值需要输入Return Value,填入return null (或者直接填入null即可,return可省略))
Force Return
允许你在调试到任意栈帧时,直接终止当前方法的执行并强行返回指定的结果,跳过后续所有代码(包括状态修改逻辑),从而中断整个请求流程。
定位请求入口栈帧
当 debug 停在某个断点时,打开 IDEA 调试面板的 “Call Stack”(调用栈) 窗口,找到当前 HTTP 请求的顶层入口方法(通常是 Controller 层的接口方法,如
UserController.addUser(...))。- 若当前断点在深层方法(如 Service、DAO 层),可通过
Drop Frame回退到上层栈帧(右键点击上层方法栈帧 →Drop Frame),逐步回退到 Controller 层(避免在深层方法强制返回导致中间层逻辑未处理)。
- 若当前断点在深层方法(如 Service、DAO 层),可通过
执行
Force Return在调用栈中右键点击顶层 Controller 方法栈帧,选择
Force Return(强制返回):- 若方法有返回值(如
ResponseEntity<String>),会弹出输入框,需指定返回值(例如输入null,或构造一个空响应对象如new ResponseEntity<>(HttpStatus.OK))。 - 若方法是
void类型,直接点击OK即可,无需输入返回值。
- 若方法有返回值(如
执行后,当前方法会立即终止,后续代码(如 Service
层的save、DAO 层的insert)不会执行,HTTP
请求会以你指定的返回值响应客户端,且不会触发后续状态修改。
Force Return
本质是强制当前方法提前退出,调试器会直接从当前栈帧返回,忽略方法内未执行的代码。
如果出现了方法里调用别的方法的情况,如果想要输入一次Force Return就彻底退出,就可以一直回退到不能再回退,脱离方法内部的方法回到顶层再Force Return
它的位置在这里
我这里离进入Controller还有114514步(spring boot环境准备,Nacos,Mysql,Redis,Redission……),所以我就找了网上的演示
注意:已执行的代码(如前序步骤的数据库更新)无法回滚,因此需在状态修改逻辑执行前使用该功能(例如在进入service.save()方法前中断)。
例:若调试到controller层时,还未调用service.updateUser(),此时Force Return可完全避免用户信息被修改;若已调用service.updateUser()并执行了
SQL,即使中断,数据库修改也已生效(需手动回滚或依赖事务)。
还注意,在测试类或 Controller
方法上添加@Transactional注解,并配置rollbackFor = Exception.class,调试时即使执行了save操作,方法结束后事务会自动回滚(需确保数据库支持事务,如
InnoDB)。
注意这个和停止调试不一样啊,停止调试终止整个应用进程,Force Return 是终止当前方法,返回指定的结果
细说智能步入
想想,一行代码里有好几个方法,怎么只选择某一个方法进入。
我们知道步入,使用Step Into (Alt + F7) 进入到方法内部
但是这两个操作会根据方法调用顺序依次进入,如果方法的流程较长,这很麻烦,而且不必要
那么智能步入就很方便了,智能步入,这个功能在Run里可以看到,Smart Step Into (Shift + F7)
它的位置在这里
例如我们继续调试 Spring 的初始化相关内容
你看这里有这么多方法
我们已经来到了
Assert.state(!environment.containsProperty("spring.main.environment-prefix"), "Environment prefix cannot be set via properties.");这步断言了,这步断言有两个方法
光标对着方法,直接 Shift + F7,会自动定位到当前断点行,并列出需要进入的方法
进入就行
为断点设置条件进行调试
这种情况一般会在多递归中常用
算法吃必备了属于是
在断点的地方右键可以打开断点条件设置窗口,这里也支持多线程的设置
在 “挂起(Suspend)” 选项中,可选择 “所有(All)”(所有线程触发断点时都暂停)或“线程(Thread)”(仅当前线程触发时暂停),适用于多线程场景下的精准调试。
在 Condition 中设置我们想要的条件就行,注意其中坑出来了
IDEA 的条件断点支持Java 语法的布尔表达式,可直接使用当前作用域的变量、方法调用、对象属性等构造条件
- 简单变量判断:
userId == 123、name.equals("admin") - 集合 /
数组判断:
list.size() > 5、array[0] == null - 方法调用:
userService.isAdmin(userId)(需确保方法可访问且无副作用)
什么意思?
假如这样一个场景,在递归算法中(如斐波那契、二叉树遍历),可通过条件断点仅在特定递归深度或参数下暂停,避免无效断点干扰,我们希望当递归到第 5 层时才会暂停,方便观察该层级的计算状态。
1 | public int fib(int n) { |
那么该输入什么呢?
n = 5?
不对,idea认为条件 n=5 与预测结果Boolean不对,会报错
所以是 设置条件n == 5,才正确
在 debug 的断点条件设置中,你设置的条件最后输出的结果应该是一个 boolean 类型的值,如果你的条件非 boolean 类型最后只是将你的语句执行了,而无法进入预期的条件。
为什么说条件断点是 oi 吃用的多,因为条件断点的表达式会在每次代码执行到该行时计算,若表达式包含数据库查询、网络请求、大循环等耗时操作,会严重拖慢调试速度。
注意,条件表达式中只能使用当前方法作用域内的变量,若引用了外层方法或其他类的变量,会提示 “无法解析符号”。
多线程调试
多线程调试,需要先掌握以下两个核心要点。
查看运行栈帧 && 切换线程
在 Threads & Variables 这个窗口,进行线程之间切换。
断点暂停方式,的选择 Thread
这个是最为重要的。
建议多线程调试,选择 Make Default,也就是默认,点击图中设为默认,后续所有断点都是 Thread,如果不选择 Thread,则无法进行线程断点追踪!所有线程将直接运行结束。
什么意思?
在 IDEA 的断点设置中,“Suspend”(暂停)选项有两个核心模式:
- All(所有线程):当断点触发时,整个应用的所有线程都会被暂停(包括主线程、其他工作线程),直到手动继续执行(如按 “下一步”)。
- Thread(当前线程):当断点触发时,只有进入该断点的线程会被暂停,其他线程会继续正常运行(不受影响)。
在多线程场景中(如并发任务、线程池处理),如果断点默认是 “All” 模式,会导致:
线程执行顺序被强行打断
假设线程 A 触发断点,此时 “All” 模式会暂停所有线程(包括线程 B、C)。但实际业务中,线程 B、C 可能需要继续执行才能触发后续断点(如线程间的协作、数据传递),强行暂停会导致调试场景与真实运行场景不一致,甚至错过关键断点。
无法追踪单个线程的完整流程
多线程调试的核心需求是跟踪某一个线程的执行路径(如线程 A 从启动到结束的所有方法调用)。若用 “All” 模式,每次断点都会冻结所有线程,切换线程调试时需要手动唤醒其他线程,操作繁琐且容易混乱。
可能导致线程 “假死” 或逻辑异常
某些线程依赖超时机制(如
wait(1000))或其他线程的信号(如notify()),若被 “All” 模式长期暂停,可能触发超时、死锁等非预期行为,干扰调试判断。
当你在某个断点的设置中选择 “Thread” 模式,并点击 “Make Default” 后,后续所有新添加的断点都会默认使用 “Thread” 模式,无需每次手动修改。
这在多线程项目中非常实用
例如主线程此时主线程正在执行 FutureTask 的
get(long timeout, TimeUnit unit) 方法,并且触发了
TimeoutException 异常。
- 核心逻辑:
FutureTask是 Java 并发包中用于异步任务结果获取的类,主线程调用get(1, SECONDS)表示 “等待异步任务结果,最多等待 1 秒”。 - 当前状态:由于异步任务在 1
秒内未完成,触发了超时异常(
TimeoutException),主线程此时停在异常抛出的断点处,用于调试超时场景下的逻辑。
有这样一个子线程,该线程正在执行 Reference 类的
waitForReferencePendingList 方法。
这是 Java 引用处理机制的底层逻辑,用于等待 JVM 的 “待处理引用列表”(Pending Reference List)非空,通常与垃圾回收、弱引用 / 软引用的处理相关。
该线程可能在后台处理对象引用的生命周期(如弱引用的回收、引用队列的入队等),属于 JVM 内部的并发辅助线程。
还发现有这样的一个子线程2
该线程正在执行 Thread 类的 sleep0
方法(这是一个 native 方法,由底层操作系统实现线程休眠)。
- 核心逻辑:
sleep0是Thread.sleep()的底层实现,负责让线程暂停指定时间(纳秒级)。 - 当前状态:该线程处于休眠状态,可能是异步任务中包含了线程休眠逻辑,导致主线程等待超时。
继续下去,切换回到主线程进行下一步
这步是在Spring Cloud 网络工具类(InetUtils)的
convertAddress
方法中,处理网络地址到主机信息的转换逻辑,属于Spring
应用启动时获取本地主机信息( hostname 和 IP
地址)的关键步骤。
- 异步提交了一个任务(通过
ExecutorService),用于获取网络地址(InetAddress)对应的主机名(getHostName)。 - 现在主线程在尝试通过
Future.get(...)方法等待异步任务的结果,若任务超时或抛出异常,会捕获并将主机名默认设为localhost,最终封装成HostInfo对象返回。
来看休眠线程
可以看到休眠线程结束,引导了另一个线程启动
来看 Reference Handler 线程,它等待 JVM 虚拟机的重置,可以看到上面的处理已经完成了
IDEA 在 Debug 时默认阻塞级别是 all,会阻塞其它线程,只有在当前调试线程走完时才会走其它线程; Thread 模式在 Remote 调试时不阻塞他人请求
若需精准调试某线程,可对目标线程的代码设置 “Thread 级挂起” 的条件断点 。例如:
- 对
Thread.sleep0方法设置条件断点Thread.currentThread().getName().contains("sleep"),仅当该线程执行时暂停; - 对
FutureTask.get方法设置条件断点timeout == 1,聚焦超时逻辑。
日志断点
IDEA 还支持 “日志断点”(右键断点 → 选择 “日志(Log)”),可在不暂停程序的情况下输出变量值,结合条件表达式使用,堪称 “无侵入式调试”。
例如,设置日志内容为"当前用户:{user.id}, 年龄:{user.age}",并添加条件user.getAge() < 18,即可在控制台实时输出符合条件的用户信息,无需暂停程序。







