该项目是模拟JMH(Java Microbenchmark Harness)的基准测试框架,自己实现了一个性能测试框架,用于测试系统在特定负载的情况下,相应时间和稳定性的表现情况。
项目功能有:
- 自动加载测试用例;
- 通过接口标记待测试类;
- 通过注解标记待测试方法;
- 通过注解实现多级配置
该项目中以下列两个测试为例:
- 测试String类中直接使用“+”字符串相加与StringBuilder的append()方法相加的效率区别
- 测试自己实现的归并排序,快速排序与Arrays.sort的效率区别
结果展示:
首先,了解一下关于JMH:
一、关于JMH:
JMH(Java Microbenchmark Harness):是专门用于代码微基准测试的工具套件。Micro Benchmark:简单的来说就是基于方法层面的基准测试,精度可以达到微秒级。当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用JMH对优化的结果进行量化的分析。和其他竞品相比——如果有的话,JMH最有特色的地方就是,它是由Oracle内部实现JIT的那拨人开发的。
关于JMH的更多相关,可以参考:CSDN博主「秦沙」的原创文章:JMH使用说明
二,可能影响性能的因素:
可能影响性能的因素:
在程序执行中,可能有多种因素会影响测试的结果,主要原因还是以下几点:
1.执行时间过短
2.实验次数太少
3.优化原因(AOT/JIT编译器优化)
4.预热
5.其他原因
三、项目准备:
3.1:@Retention注解:
注解@Retention可以用来修饰注解,是注解的注解,称为元注解。
Retention注解有一个属性value,是RetentionPolicy类型的,Enum RetentionPolicy是一个枚举类型,
这个枚举决定了Retention注解应该如何去保持,也可理解为Rentention 搭配 RententionPolicy使用。RetentionPolicy有3个值:CLASS RUNTIME SOURCE
按生命周期来划分可分为3类:
1、SOURCE——在源代码中出现:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、CLASS——在*.class
中出现:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RUNTIME——在类执行时出现:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用,一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解。这个项目中用到的都是:RetentionPolicy.RUNTIME
3.2:@Target注解:
注解@Target也是用来修饰注解的注解,是一个元注解。
@Target注解决定了注解的作用目标,即被描述的注解可以用在什么地方,
取值(ElementType)有:
取值 | 描述 |
---|---|
@Target(ElementType.TYPE) | 接口、类、枚举、注解 |
@Target(ElementType.FIELD) | 字段、枚举的常量 |
@Target(ElementType.METHOD) | 方法 |
@Target(ElementType.PARAMETER) | 方法参数 |
@Target(ElementType.CONSTRUCTOR) | 构造函数 |
@Target(ElementType.LOCAL_VARIABLE) | 局部变量 |
@Target(ElementType.PACKAGE) | 包 |
3.3:定义@Measurement注解:
定义两个参数 :
group——一共进行多少组实验
iterations——一组实验调用方法多少次
可以通过改变 group和 iterations的值来改变测试的组数和每组实验调用方法的测试:
元注解@Retention与@Target的取值分别为:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
代码如下:
package com.test.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Measurement {
//一组实验调用方法多少次
int iterations();
//一共进行多少组实验
int group();
}
3.4:定义@Benchmark注解:
标注哪些方法需要被测试,类似@Test,注解中无实际内容
元注解@Retention与@Target的取值分别为:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
代码如下:
package com.test.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Benchmark {
}
3.5:定义@WarmUp注解:
定义预热次数,默认值为0,可以在实际测试时更改
代码如下:
package com.test.annotations;
public @interface WarmUp {
//预热,默认次数为0
int iterations() default 0;
}
至此,annotation注解部分完成
3.6:定义Case接口:
除了Annotation外,还需要一个Case接口用来标记待测试的类,Case接口中不需要实际内容,只起一个标记作用,实现Case接口的类就是待测试的类,在通过注解从该类中寻找待测试的具体方法。
代码如下:
package com.test;
//通过接口标记待测试类
public interface Case {
}
四、待测试类加载:
步骤:
- 根据一个固定类,找到类加载器
- 根据类加载器找到类文件所在的路径
- 扫描路径的所有类文件
- 利用Case接口,找出需要的*.class文件
代码如下:
//待测试Case加载
public class CaseLoader {
public CaseRunner load() throws IOException, ClassNotFoundException, IllegalAccessException, InstantiationException {
String pkgDot = "com.test.cases";
String pkg = "com/test/cases";
List<String> classNameList = new ArrayList<String>();
/**
* 1.根据一个固定类,找到类加载器
* 2.根据类加载器找到类文件所在的路径
* 3.扫描路径的所有类文件
* */
ClassLoader classLoader = this.getClass().getClassLoader();
Enumeration<URL> urls = classLoader.getResources(pkg);
while(urls.hasMoreElements()){
//路径
URL url = urls.nextElement();
if(!url.getProtocol().equals("file")){
//如果不是*。class文件,暂时不支持
continue;
}
String dirname = URLDecoder.decode(url.getPath(),"UTF-8");
File dir = new File(dirname);
if(!dir.isDirectory()){
//不是目录直接返回
continue;
}
//扫描该目录下的所有*.class文件,作为所有的类文件
File[] files = dir.listFiles();
if(files == null){
continue;
}
for(File file : files){
String filename = file.getName();
String className = filename.substring(0,filename.length()-6);
classNameList.add(className);
}
}
List<Case> caseList = new ArrayList<Case>();
for(String className : classNameList){
Class<?> cls = Class.forName(pkgDot+"."+className);
if(hasInterface(cls,Case.class)){
caseList.add((Case)cls.newInstance());
}
}
return new CaseRunner(caseList);
}
private boolean hasInterface(Class<?> cls,Class<?> intf){
Class<?>[] intfs = cls.getInterfaces();
for (Class<?> i : intfs){
if(i == intf){
return true;
}
}
return false;
}
}
五、获取待测试类或方法的配置:
获取一个具体待测试的方法的配置(即 group:一共进行多少组实验
iterations:一组实验调用方法多少次),有三级配置即有三次更改配置的机会:
- 默认配置:使用@Measurement时方法已经获取了Measurement中的默认配置
若后续不再进行配置,则最终测试时方法的配置就是默认配置,不会导致配置为空。
int iterations = DEFAULT_ITERATIONS;
int group = DEFAULT_GROUP;
- 类级别进行配置:先获取类级别的配置,若发现类中已进行配置,且后续不再进行配置,则则最终测试时方法的配置就是类中的配置。
Measurement classMeasurement = bCase.getClass().getAnnotation(Measurement.class);
if(classMeasurement != null){
//若已被配置,直接赋值
iterations = classMeasurement.iterations();
group = classMeasurement.group();
}
-
最后一次配置是方法级别的配置,在此之前需要获取找到待测试的方法,再取得每一个方法的注解,若方法中进行了配置,则使用方法定义的配置,否则继续使用原来的配置进行测试。
-
最终配置获得,调用对象的实例测试方法测试:
//找到哪些方法是需要测试的方法
Method[] methods = bCase.getClass().getMethods();
for(Method method : methods){
//取到每一个方法的注解,找出需要测试的方法
Benchmark benchmark = method.getAnnotation(Benchmark.class);
if(benchmark == null){
continue;
}
//获取需要测试方法的配置
Measurement methodMeasurement = method.getAnnotation(Measurement.class);
if(methodMeasurement != null){
iterations = methodMeasurement.iterations();
group = methodMeasurement.group();
}
//调用对象的实例测试方法测试
runCase(bCase,method,iterations,group);
}
至此,经过三次查看,确定了最终的配置,完整代码如下:
class CaseRunner{
//默认配置
private final int DEFAULT_ITERATIONS = 10;
private final int DEFAULT_GROUP = 5;
private final List<Case> caseList;
public CaseRunner(List<Case> caseList) {
this.caseList = caseList;
}
//TODO:没有把WarmUp预热考虑进来
//每组实验前都应该预热
public void run() throws InvocationTargetException, IllegalAccessException {
for(Case bCase : caseList){
int iterations = DEFAULT_ITERATIONS;
int group = DEFAULT_GROUP;
//先获取类级别的配置
Measurement classMeasurement = bCase.getClass().getAnnotation(Measurement.class);
if(classMeasurement != null){
//若已被配置,直接赋值
iterations = classMeasurement.iterations();
group = classMeasurement.group();
}
//找到哪些方法是需要测试的方法
Method[] methods = bCase.getClass().getMethods();
for(Method method : methods){
//取到每一个方法的注解,找出需要测试的方法
Benchmark benchmark = method.getAnnotation(Benchmark.class);
if(benchmark == null){
continue;
}
//获取需要测试方法的配置
Measurement methodMeasurement = method.getAnnotation(Measurement.class);
if(methodMeasurement != null){
iterations = methodMeasurement.iterations();
group = methodMeasurement.group();
}
//调用对象的实例测试方法测试方法
runCase(bCase,method,iterations,group);
}
}
}
//此处插入视力测试方法测试方法runCase();
}
六、实例测试方法 测试
步骤:
- 输出实例测试方法名;
- 依次进入每组测试;
- 测试前记录当前时间,测试后再次记录,相减即是测试相应多组分别所用的时间;
- 将测试所用时间输出;
代码如下:
实例测试方法测试
private void runCase(Case bCase,Method method,int iterations,int group) throws InvocationTargetException, IllegalAccessException {
System.out.println(method.getName());
for (int i = 0; i < group; i++) {
System.out.printf("第%d次实验:",i);
long t1 = System.nanoTime();
for (int j = 0; j < iterations; j++) {
//通过 Method.invoke()调用被注解的方法,即bCase
method.invoke(bCase);
}
long t2 = System.nanoTime();
System.out.printf("耗时:%d 纳秒%n",t2-t1);
}
}
七、实例测试方法
该项目以以下点作为测试用例:
- 测试String类中直接使用“+”字符串相加与StringBuilder的append()方法的效率区别
- 测试自己实现的归并排序,快速排序与Arrays.sort的效率区别
如果需要测试其他方法,只需要在Case同级目录下定义一个实现Case接口的类,类中需要测试的方法再以注解即可。
7.1 归并排序,快速排序与Arrays.sort的效率区别:
方便起见,测试代码写在了一起,代码为:
package com.test.cases;
import com.test.Case;
import com.test.annotations.Benchmark;
import com.test.annotations.Measurement;
import java.util.Arrays;
import java.util.Random;
/**
* 测试自己实现的归并排序,快速排序与Arrays.sort的耗时对比
*
* */
@Measurement(iterations = 10,group = 3)
public class SortCase implements Case {
public void quickSort(int[] a){
//1.确定基准值
quickSortInternal(a,0,a.length-1);
}
//待排序区间是[low,high]
private void quickSortInternal(int[] a,int low,int high){
if(high <= low){
return;
}
//得到基准值最终所在的下标
int[] pivotIndexs = parition(a,low,high);
quickSortInternal(a,low,pivotIndexs[0]);
quickSortInternal(a,pivotIndexs[1],high);
}
private void swap (int[] a,int i,int j){
int t = a[i];
a[i] = a[j];
a[j] = t;
}
private int[] parition(int[] a,int low,int high){
int pivot = a[high];
int less = low;
int i = low;
int more = high;
while(i < more){
if(a[i] == pivot){
i++;
}else if(a[i] < pivot){
swap(a,i,less);
i++;
less++;
}else{
while(more > i && a[more] > pivot){
more--;
}
swap(a,i,more);
}
}
return new int[]{less-1,more};
}
public void mergeSort(int[] a){
mergeSortInternal(a,0,a.length);
}
private void mergeSortInternal(int[] a,int low,int high){
if(high <= low+1){
return;
}
int mid = (low+high) >> 1;
mergeSortInternal(a,low,mid);
mergeSortInternal(a,mid,high);
merge(a,low,mid,high);
}
private void merge(int[] a,int low,int mid,int high){
int length = high-low;
int[] extra = new int[length];
int i = low;
int j = mid;
int k = 0;
while(i<mid && j<high){
if(a[i]<=a[j]){
extra[k++] = a[i++];
}else{
extra[k++] = a[j++];
}
}
while(i < mid){
extra[k++] = a[i++];
}
while(j < high){
extra[k++] = a[j++];
}
System.arraycopy(extra,0,a,low,length);
}
//测试自己写的快速排序
@Benchmark
public void testQuickSort(){
int[] a = new int[10000];
Random random = new Random(201910713);
for(int i = 0;i < a.length;i++){
a[i] = random.nextInt(100000);
}
quickSort(a);
}
//测试自己写的归并排序
@Benchmark
public void testMergeSort(){
int[] a = new int[10000];
Random random = new Random(201910713);
for(int i = 0;i < a.length;i++){
a[i] = random.nextInt(100000);
}
mergeSort(a);
}
//测试Arrays.sort
@Benchmark
public void testArraysSort(){
int[] a = new int[10000];
Random random = new Random(201910713);
for(int i = 0;i < a.length;i++){
a[i] = random.nextInt(100000);
}
Arrays.sort(a);
}
}
7.2 String的"+"操作与StringBuilder的append()方法对比:
分别实例化一个String类和一个StringBuilder类的对象,再给他们分别执行“+”操作和“appnd()”操作即可。
代码如下:
package com.test.cases;
import com.test.Case;
import com.test.annotations.Benchmark;
import com.test.annotations.Measurement;
/**
*
* String的"+"操作与StringBuilder的append()方法对比
* */
@Measurement(iterations = 10,group = 3)
public class AddCast implements Case {
private static String StringAdd(){
String s = "";
for (int i = 0; i < 10; i++) {
s+=i;
}
return s;
}
private static String StringBuilderAdd(){
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
return sb.toString();
}
@Benchmark
public void testStringAdd(){
StringAdd();
}
@Benchmark
public void testStringBuilderAdd(){
StringBuilderAdd();
}
}
八、结果展示:
先看一看标准JMH的显示样式:
测试1:测试String类中直接使用“+”字符串相加与StringBuilder的append()方法相加的效率区别
显示结果为:
测试2:测试自己实现的归并排序,快速排序与Arrays.sort的效率区别
显示结果为:
这个自制性能测试框架基本要求已经完成,后续有机会的话会继续改进。
九、参考资料:
JMH使用说明
JMH: 最装逼,最牛逼的基准测试工具套件
注解@Retention的作用
彻底搞懂Class.getResource和ClassLoader.getResource的区别和底层原理
Java反射机制及Method.invoke详解
十、项目源码:
项目的代码我也放在了我的GitHub:
GitHub-基准测试框架