6.4 代码调试
一. Bug
与 Debug
DeBug
的方法(优先选上面一种方法):
Debug
是保证程序质量(之前是防御式编程、测试,并且其与前两者均不是最影响程序质量的核心手段)或修复程序错误的最后一个手段。
Debug
的目的是寻求错误的根源并消除它。但 debug
会占用大量的时间。
Debug
是测试的后续步骤:测试发现问题, debug
消除问题。
Debug
是困难的:
- “症状”和“原因”可能相隔很远,高耦合导致的结果
- 当其他
bug
被修复后,该bug
消失了 - 例如四舍五入造成的不准确
- 因为人工错误导致
bug
的症状难以有效追踪 - 有些症状是由计时 定时等时间原因导致的
- 难以重现出错时的输入数据
- 由于外部软硬件环境变化,导致间歇性的症状
- 分布式导致的问题
二. Debug
的过程
Debug
的过程:
- 重现
bug
- 诊断
bug
- 修复
bug
- 反思
错误定位占据了绝大部分的调试时间。
针对这种问题,研究者研究问题:自动化错误定位。但普通人常用的方法为:假设-检验(即通常的暴力搜索)。
1. 复现 bug
从最小的测试用例集开始复现错误。
- 确定有哪些因素跟
bug
相关,将这些因素找出来并变化它们的值。 - 确保你的
bug
复现环境(软件版本、软件的运行环境、输入数据)跟用户发现bug
的环境尽可能保持一致。
2. 诊断 bug
定位 bug
的手段有:
- 测量,如
logging
- 分治,或称
Wolf fence algorithm
(防狼围栏算法)
- 切片,特征值
x
出错了,找出程序中有助于计算特定值x
的部分。如:
或如下程序中tax
出错了,查看标红片段:
- 寻找差异:
(1).Leveraging VCS
充分利用版本控制系统,找出在哪个commit
之后出现了bug
症状。例如版本控制工具git
使用类似分治方法探索不同的commit
,但指定具体的一部分commit
出错需要用户来指定。
(2). 基于差异的调试:两个测试用例,分别通过/未通过。通过查找二者所覆盖的代码之间的差异,快速定位出可能造成bug
的代码行。如下图,通常执行最少的语句(6、7行)可能出现错误。
(3). 查找其他方面的差异:软硬件环境、JVM
参数配置、输入文件、…… - 符号化
debug
:符号化执行,即不需输入特定的值,使用“符号值”(而非“实际值”)作为输入,解释器模拟程序执行,获得每个变量的“符号化表达式”,从而可判断是否执行正确。
符号化执行程序的三个状态:
各变量的符号化取值
路径条件 PC
计数器
符号化执行树: - 调试器
- 其他方法
三. Debugging
工具
0. 暴力方式
假设-验证:假设原因,验证原因。可以通过技巧:
- 看内存导出文件
- 到处
println()
- 自动化调试工具
1.1 Memory dump
不同版本 Java
内存管理方式可能不同,如 Java 8
与 Java 10
的内存管理方式不同;不同语言的内存管理方式可能不同。
1.2 Stack trace
根据信息知道
- 实际抛出异常的位置。(信息第一行)
- 在您自己的代码中执行的最后一行。(信息中出现自己程序的第一行)
- 你的代码的入口点,也就是你的代码的方法在堆栈跟踪中首先被调用?(信息中心出现自己程序的最后一行)
- 此执行的入口,Junit测试。(信息最后一行)
具体获取栈内容(7.2节):
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
//or
public class WhoCalled {
static void f() {
// Generate an exception to fill in the stack trace
try {
throw new Exception();
} catch (Exception e) {
for(StackTraceElement ste : e.getStackTrace())
System.out.println(ste.getMethodName());
}
}
static void g() { f(); }
static void h() { g();}
public static void main(String[] args) {
f();
System.out.println("--------------------------");
g();
System.out.println("--------------------------");
h();
}
}
//or
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
for(Thread t : map.keySet()){
StackTraceElement[] frames = map.get(t);
//... analyze frames
}
上面的第二种方法输出
f
main
--------------------------
f
g
main
--------------------------
f
g
h
main
1.3 Printf
debugging
在程序内部各部分展示程序执行时的动态信息,比使用静态的 dump
分析更有效。
来源于 C
语言。一旦软件对外发布****,所有用于 debug
的 print
语句都要去除或禁用。
- 可以选择注释,但速度慢效率低
- 委托:
//Coding & debugging
public static void MyPrint(String in) {
System.out.println(in);
}
MyPrint("Entering callMethod ");
result = callMethod();
MyPrint("The result is " + result);
//Released
public static void MyPrint(String in) {
//System.out.println(in);
}
MyPrint("Entering callMethod ");
result = callMethod();
MyPrint("The result is " + result);
1.4 Logging
通过设定日志级别来确定要 log
哪些信息;log
结果可被多种渠道加以处理,可通过设定条件进行过滤,并输出为多种格式;可使用层次化的多个日志记录器。结构如下:
针对一个应用,为其设定全局的 logger
:
import java.util.logging.*;
Logger.getGlobal().info("File -->Open menu item selected");
/*
Results:
May 10, 2018 10:12:15 PM LoggingImageViewer main
INFO: File -->Open menu item selected
*/
关闭日志命令:
Logger.getGlobal().setLevel(Level.OFF);
使用全局 logger
导致信息混乱,可以定义自己的 logger
。
import java.util.logging .*;
private static final Logger myLogger =
Logger.getLogger("com.mycompany.myapp");
//often using class name as logger name
//Or
public class LogTest {
static String strClassName = LogTest.class.getName(); //get class name
static Logger myLogger = Logger.getLogger(strClassName);
// using class name as logger name
……
myLogger.info("XXXX");
}
对于日志的输出,可以输出到控制台(缺省)。对于日志的处理,日志处理器也需要设定日志级别。
public class LevelTest {
private static String name = test.class.getName();
private static Logger log = Logger.getLogger(name)
public void sub() {
log.setLevel(Level.FINEST);
log.setUseParentHandlers(false);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINEST);
log.addHandler(handler); //将 logger 和相应的 handler 捆绑在一起, delegation
log.severe("severe level");
log.warning("warning level");
log.info("info level");
log.config("config level");
log.fine("fine level");
log.finer("finer level");
log.finest("finest level");
}
}
除了控制台处理器 ConsoleHandler
,还可以设定其他的 handlers
:
SocketHandler
网络FileHandler
文件StreamHandler
ConsoleHandler
MemoryHandler
可以选用不同的格式:
SimpleFormatter
人类可读格式XMLFormatter
1.5 编译器警告信息
为了正确性,我们可以把编译器的 warning level
调到最高级别,消除所有 warning
;精益求精,把 warning
当 error
看待;学着把编译器当作自己的老师,搞清楚每一个 warning
,并在后续代码中避免。
1.6 Debugger
Breakpoints
设置断点
执行方式有
Step Over
Step Into
Step Return
Single-stepping
单步执行Resume operation
恢复运行Temporary breakpoints
临时断点