目录
一、环境
Pitest版本: 1.6.4
Github fork: https://github.com/YKRY35/pitest
Jprofiler工具: 11.1.5
Pitest以maven插件的方式运行。
公司使用中,单元测试使用的powermock框架,过程中发生OOM,机器内存32G。
二、概述
2.1变异测试整体流程
可以看到,主进程为了替换字节码,需要javaagent,所以会起子进程来跑覆盖率和变异。
覆盖率阶段,所有的任务都在单个子进程跑,而变异阶段,会拆分任务,到不同的子进程去跑。所以存在内存泄漏和设计问题的情况下,覆盖率阶段相比变异阶段更容易Out Of Memory。
这里分析和解决内存问题,是针对主进程的覆盖率阶段和子进程1 CoverageMinion子进程。
2.2内存溢出原因
先下结论。
-
主进程(设计问题)
主进程是设计问题,导致内存过大直到溢出。
注意这个数据结构:
private final Map<Long, BlockLocation> probeToBlock = new ConcurrentHashMap<>(); |
这个数据结构是子进程内存溢出的根源。
两个原因导致这个数据结构过大:
1. MockClassLoader
PowerMock为了环境隔离,自定义了类加载器,并且重写loadClass方法,于是不遵守父委派模型,所以同一个类在不同单侧脚本中会被重复加载。
即:
可以看到同一个UserService类,因为类加载器不同,且不遵守父委派,所以它的Class对象会反复被加载,经过transformer被插入探针。
子进程的探针是要汇报到主进程作记录的,因此probeToBlock这个数据结构会特别大。
同一个类会被反复注入探针,反复次数取决于跑到这个类的单测数。
在单测1跑完,跑单测2时,Class<UserService> @1这里头的探针就没有用了,可以释放。
2. 无用探针注入
Pitest在支持powermock时,考虑到powermock使用javassist来加载最初的class文件,可能是希望在powermock修改class之前先拿到最初的class文件,因此pitest修改了javassist的字节码。
见:
https://github.com/YKRY35/pitest/blob/master/pitest/src/main/java/org/pitest/mutationtest/mocksupport/JavassistInputStreamInterceptorAdapater.java#L64
@Override
public void visitMethodInsn(final int opcode, final String owner,
final String name, final String desc, boolean itf) {
if ((opcode == Opcodes.INVOKEINTERFACE)
&& owner.equals("javassist/ClassPath") && name.equals("openClassfile")) {
this.mv.visitMethodInsn(Opcodes.INVOKESTATIC, this.interceptorClass, name,
"(Ljava/lang/Object;Ljava/lang/String;)Ljava/io/InputStream;", false);
} else {
this.mv.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
它将javassist的ClassPath/openClassfile方法调用全部替换为自己框架内的JavassistCoverageInterceptor/openClssfile,所以在mock之前pitest就能拿到没被修改过的字节码文件了。
但是!!!注意: