This page presents exercises for software developers to use for debugging Fiji.
If you are a user looking to troubleshoot issues, see the Troubleshooting page.
Debugging 是确定问题原因和/或位置的艺术。本指南的目的是为开发人员提供使用各种调试技术来识别代码中问题的实用实践经验。
要求
由于斐济是使用 SciJava principles of project management 构建的,因此本指南假设您基本熟悉这些主题和工具,特别是:
此外,您应该:
- 安装 IDE - 本指南是根据 Eclipse 编写的。
- 如果使用 Eclipse,请安装 memory analyzer plugin。
- 克隆本指南围绕其设计的 imagej-troubleshooting 配套存储库,并将其作为 Maven 项目导入到您的 IDE 中。
- 安装jvisualvm工具。
最后,您应该阅读 Lars Vogel 的 Java Debugging with Eclipse tutorial。请注意,这些教程所涵盖的主题之间存在显着的重叠。 Vogel 的指南在 Eclipse 中布置调试界面和工具方面做得非常出色,而本指南则侧重于提供实践练习并解释何时使用这些工具的原因。 如果您发现自己感到困惑,当本教程要求您在 Eclipse 中执行某些操作(例如“开始调试”、“单步执行”)时,Vogel 的指南几乎肯定涵盖了这些内容。
不应该做的事情:打印语句
对于许多开发人员来说,调试工具箱中的第一个工具是 print 语句。打印语句很容易作为安全拐杖:您不需要任何特殊知识就可以使用它们,并且它们通常可以回答常见问题(例如“为什么这里变量为空?”,“这里我的数组中有多少个元素?”)。
然而,尝试通过 print 语句进行调试有一些严重的缺点:
- 他们很慢。如果您意识到需要移动或添加打印语句,则需要重新编译代码并重新启动应用程序。
- 它们是代码的一部分。添加打印语句会更改行号,导致 git 获取对源代码的修改,甚至会影响性能和/或行为。
- 他们是有限的。即使是 Eclipse 调试模式中最基本的断点和表达式求值也比打印语句提供了更多的功能和灵活性。
可以理解,学习使用调试工具是一种负担:作为开发人员,这是“另一件事”。但如果您想要develop plugins,您几乎肯定会遇到需要调试的情况。因此,您不妨现在就开始熟悉这些工具,获得对您的整个职业生涯都有好处的技能和观点。
使用本指南
这些练习的目标不是“解决”问题,而是建立您的故障排除技术工具箱,并培养您对“何时”应用每种技术的直觉。为了使练习简单而集中,没有人明确使用斐济。但是,一旦您学会了如何debug an external Java application,您将具备将这些技术中的任何一种应用于丰富且复杂的应用程序(如Fiji)的知识。
由于该项目旨在帮助新开发人员练习故障排除技能,因此您可能会发现这些示例是人为的 - 事实上,它们确实是人为的。练习保持简单且集中,以便练习有针对性的技术。如果您对代码有完整的了解和理解,则实际上不需要进行故障排除:了解某些行为不正确的原因是微不足道的。因此,这些练习的源代码分为 hidden 和 visible 包。强烈建议用户仅检查和设置 visible 类中的断点。从开发的角度来看,请将 hidden 包视为您可能无法控制或访问源代码的第 3 方库。
更改源代码以实际修复错误超出了本指南的范围,但当然欢迎有动力的用户这样做以进行练习。
如果任何时候您需要恢复对 imagej-troubleshooting 存储库的更改,您始终可以通过以下命令执行此操作:
git reset --hard origin/master
练习
练习 1:堆栈跟踪和断点
目标
- 解释堆栈跟踪
- 练习在 Eclipse 中设置断点
- 使用变量窗口检查变量值
- 使用导航命令在调试模式下执行代码
断点是调试的基本工具。它们提供了一种方法来指示 Java 在遇到某一行代码时停止代码执行,从而提供了探索主动运行代码的机会。
要开始本练习,请打开源文件 - E1BasicBreakpoints - 并“运行”它以了解发生了什么。我们应该看到一个简单的堆栈跟踪:

Stack traces 是调试的常见起点,因为它们通常是在程序未准备好处理的错误发生时自动生成的。 Java程序按照Last In, First Out顺序执行;也就是说,从 main 方法开始,当方法被调用时,它们被添加到堆栈的顶部,顶部的方法是当前正在运行的方法,当方法完成时,它将从堆栈中删除,将程序返回到行中的下一个方法。当异常发生时,会打印堆栈跟踪,显示方法排队的顺序,堆栈顶部是异常的位置(因此可能是开始寻找问题的地方!)。
因此,回顾我们得到的堆栈跟踪,我们可以看到什么出了问题(尝试使用 null 对象)以及哪里发生了问题(堆栈顶部的行号),但我们不知道为什么该对象当时是 null - 这将是异常的实际根本原因。
要进一步调查,请尝试完成以下调试步骤:
- 在调用
makeAThing之前,在main方法中设置断点 2.调试文件作为 Java 应用程序 - 当遇到断点时,单步执行到
makeAThing方法 4.跨过构建新Object的线路 - 走出
makeAThing方法 - 在Variables窗口中,查看Object变量的值 7.“恢复”执行直到程序完成
Quiz! 现在您已经完成了该计划,您知道为什么我们得到了NullPointerException吗?
Answer: Although makeAThing does create an new Object, that Object isn’t actually returned by the method.
外卖
- 堆栈跟踪有助于确定调试的起点
- 调试视图允许逐行执行代码并检查变量值以帮助我们查明错误
练习 2:表达式
目标
- 在异常创建时设置断点
- 使用表达式窗口在运行时评估代码
虽然断点让我们有机会查看正在运行的代码内部,但很多时候在调试时您会发现缺少当前代码未提供的一条信息 - 也许您想调用一个实用方法,或者临时构造一个新对象。所有这些操作都是 Java expressions - Java 语法中的语句解析为单个值(本质上是 - 一行代码)。在调试模式下,我们可以按需动态评估任何表达式,而不需要更改源代码,而不是在程序代码中进行这些更改并重新编译。
首先打开 E2EffectiveExpressions 源并运行它。与之前的练习一样,我们有一个堆栈跟踪可以从以下位置开始:

尝试在条件行上设置断点:
if (index < 0 || index >= list.size()) {
Quiz! Try debugging now, using Resume any time a breakpoint is encountered. How many times do you hit a breakpoint?
Answer: Three times. This is because we’re debugging inside a method that is used multiple times in our program.由于我们只对实际发生问题时的 processElementAtIndex 方法感兴趣,所以让我们尝试一些不同的方法:

Setting a breakpoint on an exception
- 从 Debug 视角的 Breakpoints 窗口中,删除旧断点
- 现在使用 Add Java Exception Breakpoint 按钮将断点添加到 IllegalArgumentException
- 调试程序。当它停止时,检查变量窗口。

Inspecting the variables window
此时,我们知道访问列表的99999th元素时出现问题,但变量窗口并没有告诉我们到底是什么问题。我们可以手动扩展和探索 list 变量 - 但考虑到它的大小,这可能会很麻烦。
相反,让我们使用表达式。如果它尚不可见,请打开 Window › Show View › Expressions 视图。
在此视图中,我们可以添加任意数量的 Java 表达式进行计算。我们可以从当前作用域可见的任何变量和类中调用方法。
If you evaluate expressions that change the state of variables, for example List.add, then those changes will persist. Sometimes you do legitimately need to alter the state of variables while debugging (for example, to force the conditions in which a bug appears). Just be aware that you could also be introducing problematic behavior when evaluating expressions.
要完成此练习:
- 编写一个表达式来调用列表中的
size()方法。 - 编写第二个表达式,其计算结果为
index变量的值
Quiz! 一旦你计算了这些表达式,你能说出程序中出了什么问题吗?
Answer: We asked for a list of size 100,000 but a list of size 99,999 came back. Since we used hard-coded indices, instead of size-relative (e.g.list.size()-1), a method was called wanting to access a non-existent element.
要点
- 在异常上设置断点可以避免不必要的断点命中(当我们不确定在哪里设置断点时可能很有用)
- 调试视图的表达式窗口允许我们计算任意 Java 表达式,而无需修改源代码
练习 3:条件断点
目标
- 创建一个断点,在指定次数的命中后触发
- 创建一个断点,当特定条件成立时触发
每当执行相应的行时,断点就会触发,这对于重复的代码块来说可能是不受欢迎的。仔细考虑断点放置可能就足够了 - 在异常上或在条件块内。但是,当这些选项不可用时,我们可以通过仅在有感兴趣的内容时触发来使断点更加强大。
首先打开 E3ConditionalCrisis 源并运行它。这次我们的控制台输出看起来有点不同:

除了异常堆栈跟踪之外,程序本身似乎还发现了无效对象,导致处理未完成。尽管我们可以在异常上设置断点,就像我们在第 exercise 2 中所做的那样,但异常实际上是在程序中更有趣的部分(循环)之后发生的。正如我们在练习 2 中了解到的,重复调用的代码中的断点很烦人,因此让我们看看通过向断点附加条件可以找到什么。
首先在 everythingIsOK 赋值之后的行上设置一个断点:
everythingIsOK = ObjectAnalyzer.processElementAtIndex(myArray, i);
i++;
然后尝试以下操作:

Setting a hit count
- 打开断点窗口
- 右键单击我们的断点并选择 Breakpoint Properties…
- 选中Hit Count 框并将其设置为错误消息中打印的对象编号。
- 尝试调试
Quiz! 当/如果你的断点被击中时,当前对象是否有问题?
Answer: If there was, try it again! In this exercise, the “broken” object index is non-deterministic—so it is unlikely to be exactly the same index two runs in a row. If the broken object appears later the second time, your breakpoint will hit but the current object will likely be fine. If the “broken” object appears earlier the second time, your breakpoint won’t be hit at all.如果错误是确定性的,则使用基于 count 的条件断点可能非常有用。在这种情况下,我们需要尝试一些不同的东西。我们知道 everythingIsOK 标志反映了给定索引处对象的完整性 - 因此我们真正想要使用的是一个断点,当 everythingIsOK 标志设置为 false 时,该断点在循环中停止。幸运的是,断点有一个可选的“条件”标志 - 我们可以在其中输入任何解析为布尔值的 Java 语句。尝试一下:

Setting a conditional expression
- 再次打开断点窗口 2.打开我们断点的属性
- 取消选中点击次数框
- 选中Conditional 框和“Suspend when ‘true’”
- 输入我们要检查的条件 6.再次尝试调试
仅当遇到问题时才能让断点在循环中停止吗?
Quiz! 该索引处的对象有什么可疑之处?
Answer: The object wasnull.
要点
- 如果多次调用有问题的代码,则在断点上设置命中计数非常有用
- 如果问题随机出现,在断点上使用条件表达式会有所帮助
练习 4:Fiji 插件
目标
- 在 Eclipse 中启动连接到正在运行的 Fiji 实例的调试会话
练习 1-3 是抽象的、独立的程序。另一方面,E4RemoteResearch类是实际的斐济plugin类。尽管插件开发人员通常会设置测试来在斐济运行他们的插件,但不可能测试所有可能的运行时环境:用户可能启用了各种update sites,安装了自定义插件等……并且每次修改都是依赖偏差和与插件冲突的机会。当问题确实出现时,能够调试 Fiji 本身的运行副本是非常宝贵的。
E4RemoteResearch仍然有一个main方法来演示该插件的预期输出 - 在这种情况下,只需打印ConsoleService的具体实现。在 Eclipse 中运行该类,您应该在控制台中看到一些简单的输出:
I found our console service! Look: class org.scijava.console.DefaultConsoleService
接下来,我们要在斐济运行这个插件,看看会发生什么:
- 在命令行上,从
imagej-troubleshooting目录运行mvn clean install,构建.jar - 将生成的 jar(例如
target/imagej-troubleshooting-0.1.0-SNAPSHOT.jar)复制到斐济安装的jars目录中 - 出发斐济
请注意,插件的菜单路径是在类的注释中指定的:
@Plugin(type = Command.class,menuPath = "Plugins>Troubleshooting>E4 - Print ConsoleService")
因此,您现在可以通过菜单或search bar运行E4 - Print ConsoleService命令。你应该得到一个例外:

为了将 Eclipse 连接到斐济,我们需要关闭正在运行的实例和launch Fiji from the command line,这允许我们设置debug flag,例如:
Fiji/fiji --debugger=8000

Remote Java Application debug configuration
这将以能够与 Eclipse 通信的模式启动 Fiji。接下来我们需要将 Eclipse 连接到正在运行的 Fiji 实例:
- 在 Package Explorer 中右键单击
E4RemoteResearch源文件 - 选择Debug As › Debug Configurations…
- 向下滚动配置列表(或搜索),直到找到
Remote Java Application - 双击
Remote Java Application创建调试配置 - 将配置重命名为
E4RemoteResearch-remote以进行区分。如有必要,您可以更新端口以匹配启动 Fiji 时指定的端口。 - 单击
Debug按钮启动远程调试会话
此时 Fiji 和 Eclipse 应该可以进行通信。重要的是要理解信息流从 Fiji 到 Eclipse:Fiji 说“我位于 Y 类中的第 X 行”,如果 Eclipse 查找它所知道的源文件,如果在该位置找到断点,就会停止。
但是,当 Eclipse 查看其源文件时,它并没有查看用于启动远程应用程序的实际类。事实上,Eclipse 查找的类完全取决于项目的类路径用于启动远程调试会话。因此,当您进行远程调试时,有两个最佳实践可供遵循:
- 从您感兴趣的项目启动远程会话(如果您想调试
scijava-common中的类,请勿从imagej-legacy项目启动会话) - 确保 Eclipse 中的源与远程应用程序中的源匹配(保证这一点的简单方法是构建
.jar并将其复制到应用程序)
由于我们已经遵循了这些最佳实践,我们现在终于可以调试我们的插件了:
- 在 Eclipse 中,在将
ConsoleService转换为DefualtConsoleService的行上设置一个断点 - 在斐济,运行
E4 - Print ConsoleService命令 - 在 Eclipse 中,命中断点时,检查
consoleService字段的值
Quiz! consoleService是什么类别?
Answer: It’s a LegacyConsoleService.
Quiz! 额外加分:为什么当我们运行它的 main 方法时这个插件可以工作,但在斐济却不行?
Answer: The Context in the main method is built with only one ConsoleService implementation available - DefaultConsoleService. In the full Context used in Fiji, a higher-priority LegacyConsoleService overrides the DefaultConsoleService.
要点
- 从命令行启动 Fiji 允许我们添加有用的标志并收集调试信息
- 将 Eclipse 附加到正在运行的 Fiji 可以让我们在“生产”环境中调试插件
练习 5:Git 历史记录
目标
- 练习使用git bisect来定位历史破损
通过我们迄今为止讨论的技术直接调试代码并不总是可行的。如果代码过于复杂,或者问题很微妙,尝试逐步执行代码可能不切实际 - 您甚至可能不确定从哪里开始。保持代码良好的结构和良好的文档记录可以在这里有所帮助,但我们还有另一个资源可以利用:通过 git 提交对代码进行更改的历史记录 - 假设使用了 appropriate development practices。识别重大变化为我们提供了更多可用于诊断的信息(在某些情况下,可以通过简单的 git revert] 进行修复)。
因此,我们要做的第一件事是运行 E5HistoricalHysteria 类并验证它是否失败。对于本练习,我们有一些附加信息:此类曾经成功运行,并且当时创建了 tag。
要查找可用的标签,我们可以在命令行上运行:
$ hinerm@Nyarlathotep ~/code/imagej/imagej-troubleshooting (master)
git fetch --tags
$ hinerm@Nyarlathotep ~/code/imagej/imagej-troubleshooting (master)
git tag -l
e5-good-maths
bisect工具的目的是搜索我们的提交历史记录以找到中断的提交。虽然您可以通过提交历史记录进行一项一项搜索,但这将需要与要搜索的提交数量成比例的 Time complexity#linear-time。二等分执行Binary search algorithm#Performance,显着加快了该过程。它还允许大部分过程实现自动化。
要使用二分法工具,我们只需要知道两次提交:一次测试有效,一次测试失败。在这种情况下,我们知道 e5-good-maths 标签有效,并且我们当前的状态已损坏,因此我们可以开始二等分:
$ hinerm@Nyarlathotep ~/code/imagej/imagej-troubleshooting (master)
git bisect start master e5-good-maths
Bisecting: 9 revisions left to test after this (roughly 3 steps)
[d2e45589ca3671c93f772d4310fed9653ca1569b] E5: Zach likes the maths!
$ hinerm@Nyarlathotep ~/code/imagej/imagej-troubleshooting ((d2e4558...)|BISECTING)
在二分法的每一步中,git 都会自动将您移至要测试的新提交。 “测试”仅涉及尝试重现错误并让 git 知道此提交是好还是坏。在二分时,整个本地存储库处于特殊的“二分”模式 - 以便 git 可以记住每次提交的答案。如果您犯了错误(错误地标记了提交),您可以通过 git bisect reset 中止该过程。
现在您已经进行了二等分,请按照以下步骤操作,直到确定失败的提交:
- 在 Eclipse 中,刷新
imagej-troubleshooting项目,以确保当前提交是正在测试的内容 2.运行E5HistoricalHysteria类 - 在命令行上,根据是否引发异常,使用
git bisect bad或git bisect good
将最后一次提交标记为好或坏后,bisect 将打印出第一个坏提交。您可以使用git bisect reset来完成二等分。
git bisect 确定的第一个错误提交是什么?Answer: ``` commit 3102e5620a61978b52a733a0733c83899aeddc66 Author: Mark Hiner hinerm@gmail.com Date: Fri Nov 20 13:46:34 2015 -0600
E5: More maths plz! ```</span>
</details>
要点
- 调试时使用所有可用资源 - 甚至 git 历史记录也很有用(假设它是 well-maintained!)
练习 6:打印堆栈跟踪
目标
- 在没有给出反馈的情况下识别有问题的代码
调试时,我们试图找出程序未按预期运行的原因。通常,这会响应未处理的 Java 异常,该异常会附带有用的堆栈跟踪来为我们指明正确的方向。不幸的是,有时没有给出任何信息 - 例如当JVM hangs (gets stuck)或crashes without warning时。在本练习中,我们将研究另一种从应用程序中提取信息的方法:强制打印堆栈跟踪。
正如我们在in exercise 4中所做的那样,首先要做的是构建imagej-troubleshooting.jar并将其安装在Fiji/jars目录中。然后您可以启动 Fiji 并运行此练习的命令:Plugins › Troubleshooting › E6 - Start Looping
运行此命令后,您应该注意到 Fiji 静置了几秒钟……然后意外关闭。由于这次崩溃没有给我们任何线索,我们接下来应该做的就是查看代码:
NotALoop.dontLoopTwice();
NotALoop.dontLoopThrice();
NotALoop.dontLoopForever();
NotALoop.loopForever();
我们看到正在调用四个方法。他们做什么?不知道!但我们知道其中之一(至少)是不好的。因此,让我们尽我们所能来了解应用程序在崩溃之前发生了什么。
要进一步调查,请关闭 Fiji(如果正在运行)并从命令行再次启动它,例如:
Fiji/fiji
这次我们实际上不需要任何额外的标志,因为这种技术并不是斐济特有的。当您从命令行运行程序时,您的控制台直接与正在运行的实例绑定:
Waiting for input after launching Fiji

在这种状态下,我们仍然可以向正在运行的应用程序发送信号(例如 - ⌃ Ctrl + C 至 kill the app)。<div class="notice notice-tip" style="font-size: 2;"><div class="notice-icon">💡</div><div class="notice-content"><p>运行 Java 应用程序时,我们可以使用⌃ Ctrl + \(在 Windows 上为⌃ Ctrl + Pause)来打印堆栈跟踪。有关详细信息,请参阅第 print stack trace instructions。</p> </div> </div>
有了这些知识:
- 从斐济运行
E6 - Start Looping命令 - 在 Fiji 崩溃之前,切换回终端并使用 ⌃ Ctrl + \ 打印堆栈跟踪
- 因为我们想猜测最后运行的方法是什么,所以继续获取堆栈跟踪,直到 Fiji 崩溃
- 回看控制台文本,找到最后执行的方法
提示:像这样的原始堆栈转储不是最容易阅读的。 JVM 中所有线程的堆栈跟踪以及我们不感兴趣的其他信息都会被打印。查找按线程排序的堆栈跟踪部分(就像您在异常消息中看到的那样),并找到 E6SleuthingSilence 类。该条目后面的任何内容都位于堆栈顶部,因此当您获取堆栈跟踪时,该线程上正在处理的内容。
Quiz! 您确定崩溃前最后执行的方法是什么?
Answer:net.imagej.trouble.hidden.NotALoop.dontLoopForever is what you should find. Note that there is some hand-waving in this exercise: it’s possible that this method could have returned and a subsequent method caused the actual crash! But we at least have gained information, in that we know the dontLoopForever method was executed.
要点
- 当我们没有其他事情可以继续时,手动获取堆栈跟踪非常有用(例如,如果我们的程序在没有警告的情况下崩溃,或者变得无响应)
练习 7:内存不足
目标
- 了解如何获取堆转储
- 分析堆转储是否存在潜在问题
记忆问题是另一种野兽。 Java 不断地回收不再使用的对象(garbage collection)。如果某些东西在不应该的情况下错误地保留了对象,则会出现内存泄漏(例如,用户关闭图像,但保留像素数据数组)。根据泄漏的大小,甚至可能不会遇到(例如,仅在打开 10,000 张图像后)。
内存错误可能特别难以重现,因为一旦内存用完,任何对象创建都可能触发OutOfMemoryError。因此生成的堆栈跟踪可能会产生误导。相反,我们想要做的是检查 Java 堆空间(存储对象实例的位置)并找出问题所在。
首先,运行 E7InvestigateImpressions 看看会发生什么。您应该会看到它打印数组名称一段时间,然后最终出现 OutOfMemoryError。
看一下代码片段:
for (int i = 0; i < 100; i++) {
System.out.println(ObjectMaker.getFloatArray(size));
System.out.println(ObjectMaker.getLongArray(size));
System.out.println(ObjectMaker.getDoubleArray(size));
}
我们看到正在创建对象,但我们没有存储对它们的任何引用。它们应该被打印并丢弃。因此,推测其中一种方法正在做一些令人讨厌的事情,这取决于我们通过分析堆空间来找出是哪种方法。特别是,我们想要分析发生错误时的堆空间。 jvisualvm用于附加到正在运行的Java程序;因此,我们需要在 OutOfMemoryError 本身上设置一个断点 - 类似于我们在 Exercise 2: Expressions 中所做的 - 并调试 E7 程序。
一旦遇到OutOfMemoryError,我们的断点就会触发。要获取堆转储:

Heap dump acquisition in jvisualvm
- 打开
jvisualvm - 从本地应用程序列表中,右键单击
net.imagej.trouble.visible.E7InvestigateImpressions,然后选择“堆转储”选项。 3.默认打开“摘要”视图;切换到堆转储的“类”视图 - 按尺寸排序
Quiz! 哪个类占用了大部分内存?
Answer:java.lang.Float[]
所以现在我们知道是什么占用了我们所有的记忆 - 但我们实际上不知道为什么。虽然jvisualvm确实有进一步研究的工具,但 Eclipse 的内存分析器插件为进一步的堆转储探索提供了一些很好的便利。我们来看一下:
- 在
jvisualvm的应用程序列表中选择堆转储后,使用File › Save As…保存.hprof文件。 - 返回 Eclipse,打开“Memory Analysis”透视图(Window › Perspective › Open Perspective › Other… › Memory Analysis,如果您以前从未打开过此透视图)
- File › Open Heap Dump…并选择您在步骤 1 中创建的文件
- 在堆转储概述的
Actions列下,单击Histogram- 这将在新选项卡中打开 - 根据您在
jvisualvm中确定的有问题的类别过滤直方图的类别 - 右键单击有问题的类所在的行,然后选择“合并到 GC 根的短路径 > 排除弱/软引用”选项。
堆转储中有大量可以浏览的信息。通过右键单击上下文菜单提供的大多数选项都是 SQL 查询的快捷方式。当我们查看“GC 根路径”时,我们查看的是使所选类保持活动状态的对象之间的引用链。因为我们正在调查内存泄漏,所以我们通常可以排除弱引用和软引用,因为这些引用应该在 OutOfMemoryError 发生之前释放。
Quiz! 展开“合并最短路径”选项卡中的类,直到到达 ObjectMaker 类的第一个子类。子类的变量名称和类是什么?
Answer: It is a java.util.HashSet with the name cache. ObjectMaker seems to store the Float[] instances in a set, creating strong references that prevent the arrays from being garbage collected.
In this exercise we acquired the heap dump manually via jvisualvm. When you right-click on an application you can also set heap dumps to be acquired automatically when OutOfMemoryErrors occur. Heap dumps can also be acquired directly through the Eclipise MAT plugin, in the Memory Analysis perspective.
要点
- 为了识别微妙的问题,例如内存泄漏,它有助于学习如何使用外部工具 - 例如
jvisualvm
练习 8:分析
目标
- 识别性能瓶颈
在其他调试练习中,我们会研究由于错误而失败的程序。另一个常见的问题是程序性能 - 您的程序是否在合理的时间范围内运行?这通常不能通过简单地查看代码来推断出来。因此,修复性能的第一步是确定速度下降的位置。
练习 8 非常简单,主函数调用两个函数 - doStuff() 和 doMoreStuff()。这两个函数都被依次调用,持续两分钟。我们的目标是找出哪个函数比另一个函数慢。程序员可能想到的一种解决方案是通过比较 System.currentTimeMillis 返回的值来跟踪所花费的时间,但这种方法不能扩展到大型程序,并且需要修改代码。使用探查器来监视方法所花费的时间更加高效和有用 - 这是jvisualvm故障排除工具的一部分。
锻炼步骤
1、在main方法中的while语句处插入断点
2.启动JVisualVM。所有正在运行的 Java 应用程序都显示在 JvisualVM 的“应用程序”选项卡(左边距)中。双击选择 E8PerceivingPerformance。转到分析器选项卡并单击 CPU 选项。
- 单击Jvisualvm右上角的设置复选框。在 CPU 设置中,确保要分析的类是
net.imagej.trouble.**而不是net.imagej.trouble.visible.**,否则分析器不会在正确的位置查找
Adjust settings
</figcaption></figure> 4.切换到Eclipse并恢复代码的执行 5.等待规定时间,转动拇指。或者你可以切换到JvisualVM并实时查看每个函数花费了多少时间。
- 输出示例

Profiling Results
</figcaption></figure>
Quiz! 哪种方法需要更多时间?doStuff 还是 doMoreStuff?
Answer: doStuff. Exact timing will vary per computer, but in our case doStuff took 954 ms while doMoreStuff took 710 ms.
Quiz! 两个函数的调用次数/函数调用次数是否相同?
Answer: Yes. This number will change based on the length of profiling, but in this case we see both methods were called 83,573,448 times - so the difference in timing is truly due to length of method execution.要点
- 分析工具可以快速准确地识别性能瓶颈。
练习 9:多线程
目标
- 观察多线程环境下调试的效果
开发代码通常可能涉及使用多个线程而不是顺序执行语句。需要时可以随时生成新线程。本质上,线程应该用于运行独立的进程,但如果线程在同一进程上工作,则不能保证不同线程中的语句按特定顺序执行。
E9 多线程练习重点讨论了这个问题,并强调了断点的另一个属性,它有助于调试程序(停止虚拟机)。
即使没有代码更改,有时调试也会影响代码执行。练习 9 创建了两个相互依赖的并行进程。如果另一个进程引入超过一秒的延迟,每个进程都会抛出错误。因此,在调试程序时暂停超过一秒会引发异常。因此,在这种情况下,调试行为会引入错误。 
练习步骤
- 不带断点运行 E9。请注意,它工作正常
- 现在在 Even 或 Odd LonelyRunnable 的“getName”方法中设置断点
- 将断点属性设置为“挂起线程”
- 调试E9。到达断点后,等待几秒钟。最终您应该看到另一个线程已暂停并且“期待异常”。
- 恢复线程并查看堆栈跟踪。
- 现在修改断点并告诉它挂起VM 而不仅仅是线程。
- 再次调试。当遇到断点时,请确保恢复虚拟机而不仅仅是线程。
要点
- 调试行为可以改变代码的执行方式。针对具体情况找到正确的方法。
- 如果是多线程编程,设置断点,让虚拟机在断点处停止,冻结所有线程的状态。
接下来怎么办?
在本指南的开头,我们提到调试的目标并不是明确解决问题。因此,接下来的一个自然问题是 - 一旦我们成功确定了问题的原因(或至少缩小了范围),接下来要采取什么步骤?
答案取决于调试开发人员的技能和职责,以及他们是否已确定修复方案(一旦发现问题,通常很容易找到修复方案)。
如果确定修复:
- 如果您是组件的维护者,您可以直接进行修复,或者如果修复需要首先讨论,则使用 topic branch。
- 如果您没有存储库的提交权限,您可以通过拉取请求contribute the fix
如果修复不清楚:
- 使用任何标准channels for help。
即使您无法提供修复程序,如果您付出了调试的努力 - 至少您应该通过 bug report 确定问题、调试步骤以及潜在的修复程序,这样您的努力就不会白费。