Jacoco的覆盖率原理
收集覆盖率信息的方法
Runtime Profiling
Runtime Profiling是一种在程序运行时进行的性能分析技术,它可以帮助开发者了解程序的运行情况,识别性能瓶颈和优化程序性能。由于是在程序运行时进行,runtime profiling 能够提供实时的数据,便于理解程序在实际运行条件下的行为。JVMTI(Java Virtual Machine Tool Interface)是一个由Java虚拟机(JVM)提供的原生编程接口,它用于开发可以监控和控制JVM内部状态的工具。JVMTI是JVMPI(Java Virtual Machine Profiler Interface)和JVMDI(Java Virtual Machine Debug Interface)的后续版本,提供了更丰富的功能和更高效的性能。JVMTI提供了广泛的功能,包括但不限于调试、性能分析、线程分析、内存分析等。JVMTI是一个原生接口,这意味着它需要用C或C++等语言来编写代理程序(Agent),这些代理程序可以与JVM交互。JVMTI允许代理程序注册对特定事件的兴趣,如类加载、方法调用、异常抛出等,JVM会在这些事件发生时通知代理程序。代理程序可以通过JVMTI获取JVM的运行时数据,包括堆内存使用情况、线程状态、类信息等。除了监控,JVMTI还允许代理程序在一定程度上控制JVM的行为,例如暂停和恢复线程的执行。JVMTI是跨平台的,可以在不同的操作系统和硬件架构上运行。虽然JVMTI提供了强大的功能,但是需要注意的是,并非所有的JVM实现都支持JVMTI的所有特性。JVMTI与JDWP(Java Debug Wire Protocol)和JDI(Java Debug Interface)一起工作,形成了JDPA(Java Debugging and Profiling Architecture),这是一个完整的调试和性能分析体系。使用JVMTI可能会对JVM的性能产生一定影响,尤其是在启用了大量事件通知的情况下。JVMTI提供了对JVM内部状态的深入访问,因此在设计和实现基于JVMTI的工具时需要考虑安全性和稳定性。
Instrumentation
通过Instrumentation可以动态对运行中的Java程序进行操作,Intrumentation的api都在java.lang.instrument包里,可以实现性能监控、代码覆盖分析、动态追踪等功能。代码覆盖可以通过ASM技术在bytecode(字节码)中注入,从而实现覆盖率的统计,Java中比较有名的Jacoco就是通过这种方式实现的。
Jacoco
Jacoco的Probe探针的原理
Jacoco目前支持on-the-fly模型和offline模式两种注入形式。on-the-fly模型通过指定-javaagent的参数指定特殊的Jar包启动instrumentation代理程序,这个代理程序通过Class Loader 装载一个class前判断是否转换修改class文件,将统计代码注入到class中,测试覆盖率分析在JVM执行测试代码的过程中就完成了。offline实现对被测试代码生成已经完成插桩的变异后文件(class或者jar),在测试环境部署插桩后的文件,这样就可以完成覆盖率的收集工作了。Jacoco的字节码注入方式通过Probe探针的方式注入。
Jacoco是根据控制流Type采取不同的探针插入策略的,一个用java字节码定义的java方法的控制流图可能有以下的Type,每一个Type连接一个源指令与目标指令,Type不同探针的注入策略也会不同,如下是Type定义:
type | source | target | remarks |
---|---|---|---|
ENTRY | First instraction in method | ||
SEQUENCE | Instruction, except GOTO, xRETURN, THROW, TABLESWITCH and LOOKUPSWITCH | subsequent instruction | |
JUMP | GOTO, IFx, TABLESWITCH or LOOKUPSWITCH instruction | Taget instruction | TABLESWITCH and LOOKUPSWITCH will define multiple edges |
EXHANDLER | Any instruction in handler scope | Target instruction | |
EXIT | xRETURN or THROW instruction | ||
EXEXIT | Any instruction | unhandled exception |
如果是每一个字节码都插入一个probe探针,那么最终的class会比原来大好几倍,所以这个probe探针是按照如下几种情况进行插入的(红色边是需要插入探针的流程)。
- SEQUENCE:在一个简单的序列探针插入两个字节码指令之间。
- JUMP(unconditional):无条件跳转的指令,就放在GOTO指令之前
JUMP(conditional):反转操作码的语义,然后在条件跳转指令后加探针,然后在探针后添加GOTO指令跳转到原本的位置(这是因为字节码是顺序执行的,所以需要添加一个goto,完成无条件跳转的。)下图是Jacoco给的解释示意图。
如果对应的JUMP是包含IF和ELSE的逻辑,那么如下的图更容易理解。EXIT:RETURN or THROW在这些语句之前添加探针。
Jacoco动态注入
public class JacocoTest {
private static transient /* synthetic */ boolean[] $jacocoData;
public JacocoTest() {
boolean[] arrbl = JacocoTest.$jacocoInit();
arrbl[0] = true;
}
public static void main(String[] arrstring) {
boolean[] arrbl = JacocoTest.$jacocoInit();
int a = 10;
++a;
arrbl[1] = true;
System.out.println();
if (++a > 10) {
arrbl[2] = true;
JacocoTest.test1();
arrbl[3] = true;
} else {
JacocoTest.test2();
arrbl[4] = true;
}
System.out.println();
arrbl[5] = true;
}
public static void test1() {
boolean[] arrbl = JacocoTest.$jacocoInit();
System.out.println("");
arrbl[6] = true;
}
public static void test2() {
boolean[] arrbl = JacocoTest.$jacocoInit();
System.out.println("");
arrbl[7] = true;
arrbl[8] = true;
throw new RuntimeException("");
}
private static /* synthetic */ boolean[] $jacocoInit() {
boolean[] arrbl = $jacocoData;
boolean[] arrbl2 = arrbl;
if (arrbl != null) return arrbl2;
Object[] arrobject = new Object[]{4473305039327547984L, "com/xin/test/JacocoTest", 9};
UnknownError.$jacocoAccess.equals(arrobject);
arrbl2 = $jacocoData = (boolean[])arrobject[0];
return arrbl2;
}
}
从上面代码可以看出,Jacoco是用来一个boolean数组作为标记(boolean[] arrbl = JacocoTest.$jacocoInit();),只要执行过对应代码就对这个数组赋值True,这里也能看出并不是每一行都给出了标记。Probe探针在不影响原来的指令执行流程的前提下插入到两个指令之间,每一个探针都插入在程序的control flow的边中,如果探针被执行了,我们可以确定这个边上的代码就会被执行。Jacoco官方介绍文档
Jacoco的常用命令
instrument
Instrument命令是实现代码覆盖率测量的关键步骤之一,主要实现了Java字节码中插入代码覆盖数据收集的探针(Probe)。
java -jar jacococli.jar instrument [<sourcefiles> ...] --dest <dir> [--help] [--quiet]
<sourcefiles>:原文件
--dest <dir> :将加入探针后注入的classes存入<dir>
--quiet :执行过程中不会让控制台(标准输出stdout)打印信息
举例:java -jar jacococli.jar instrument C:\test.jar --dest classes
dump
dump命令使用Jacoco CLI连接到远程服务端,请求覆盖率数据,并将其保存到本地文件。运行dump的服务器是客户端,运行agent的是服务端。
java -jar jacococli.jar dump [--address <address>] --destfile <path> [--help] [--port <port>] [--quiet] [--reset] [--retry <count>]
--address <address>:server的IP地址
--destfile <path> :exec文件存储位置
--port <port> :server的port,默认是 (default 6300)
--quiet :执行过程中不会让控制台(标准输出stdout)打印信息
--reset:重置覆盖率报告(exec文件),在转存完当前的覆盖率数据之后,会清空之前收集的数据,以便从下一次执行开始重新收集。
--retry <count> :用于指定在无法连接到Jacoco Agent时,CLI应该尝试重连的次数。 (默认是 10)
举例:java -jar jacococli.jar dump --address 192.168.0.4 --destfile test.exec
merge
合并多个exec报告文件,生成一个新的文件。官方明确说过,merge只能是相同代码生成的exec,主要是为了解决多副本的部署服务,由于请求经过负载均衡会在不同机器上产生exec文件,因此通过merge可以完成该类exec的文件。
java -jar jacococli.jar merge [<execfiles> ...] --destfile <path> [--help] [--quiet]
<execfiles> :需要合并的exec文件
--destfile <path>: 合并后的报告输出目录
--quiet :执行过程中不会让控制台(标准输出stdout)打印信息
举例:
java -jar jacococli.jar merge *.exec --destfile merge/all.exec
report
依据exec和classes文件生成覆盖率报告
java -jar jacococli.jar report [<execfiles> ...] --classfiles <path> [--csv <file>] [--encoding <charset>] [--help] [--html <dir>] [--name <name>] [--quiet] [--sourcefiles <path>] [--tabwith <n>] [--xml <file>]
<execfiles>:需要合并的exec文件
--classfiles <path>:class文件
--csv <file> :CSV报告
--html <dir> :html报告
--name <name> :报告名字
--quiet :执行过程中不会让控制台(标准输出stdout)打印信息
--sourcefiles <path>:这个参数告诉 Jacoco 在生成报告时包括源代码文件。这样,报告中会显示源代码,并在被执行的代码行上打上标记,通常以不同的颜色或样式来区分。
--tabwith <n> :源页面的制表符宽度(默认为 4)
--xml <file> :xml格式报告
举例:java -jar jacococli.jar report test.exec --classfiles .\test.jar --html report.html --sourcefiles .\test\src\main\java
execinfo
以可读的方式打印exec文件的内容。
java -jar jacococli.jar execinfo [<execfiles> ...] [--help] [--quiet]
<execfiles>:需要合并的exec文件
--quiet :执行过程中不会让控制台(标准输出stdout)打印信息
举例:java -jar jacococli.jar execinfo *.exec
exec文件详解
使用execinfo解读出来的exec文件如下:
如上图所示:
- 第一列是classid,例如8adcfe1de92e357b,这个classid是通过原始类文件的CRC64校验和来创建的,所以如果代码有修改,这个classid比如会变化。
- 第二列是覆盖探针的统计信息,例如90 of 272,表示这个类插入Probe探针272个,覆盖了90个。
- 第三列是类名,例如org/springframework/boot/logging/logback/ExtendedWhitespaceThrowableProxyConverter。