序
今天突然想起,去年面试的时候 还真被问过一个问题,String StringBuilder 拼接哪个更快
反正我当时是没答出来,说实话 如果不是这个面试,我估计一直关注不到
这个比较不是重点,对于性能比较 使用这个工具
一般需要性能比较 等知识点的时候,我们首先都是百度一下,然后可能会立刻知道结果,但是 不能人云亦云,人家说什么信什么,我们也需要自己 能测试 ,能拿出证据,让别人信服
这里就要说下 JMH 性能测试框架,下面是一个简单例子,只是为了演示所用
JMH 整合
pom
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.23</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.23</version>
</dependency>
关闭 JIT
防止 jit 导致的 优化 影响性能测试结果
测试 String StringBuilder 拼接哪个更快
内存都知道,StringBuilder 内存占用少,那哪个快呢?
测试代码
@Benchmark public void testStringBuffer() { StringBuffer sb = new StringBuffer(); sb.append("xxxxxa"); sb.append("-"); sb.append("qweqw123123"); }
@Benchmark public void testStringBuilder() { StringBuilder sb = new StringBuilder(); sb.append("xxxxxa"); sb.append("-"); sb.append("qweqw123123"); }
@Benchmark public void testStringAdd() { String s1 = "xxxxxa"; String s2 = "-"; String s3 = "qweqw123123"; String s4 = s1 + s2 + s3; }
我靠,这按照道理 不是 Stringbuilder 更快么? 反正我当时 八股文是这么背的
我本以为应该是 StringBuilder 最快呢,这怎么 String 拼接最快。。。。
Stringbuild
String
String的是不能变化的final,他的拼接是 生成新的 char[] 数组,对内存占用可能比较多
Stringbuilder 的append 是 在同一个char[]的基础上 拼接
从这想
那可能 拼接的String 越多,StringBuild 才能越体现出来优势,改下代码
循环次数 改为100w次,再次测试
Benchmark Mode Cnt Score Error Units
ABenchmarkTest.testStringAdd thrpt 3 150.645 �� 24.663 ops/s
ABenchmarkTest.testStringBuffer thrpt 3 109.808 �� 90.552 ops/s
ABenchmarkTest.testStringBuilder thrpt 3 108.064 �� 100.271 ops/s
这还是 String 拼接快啊,当然了这里没说内存的事,把次数 放到 1000w次,再看看
Benchmark Mode Cnt Score Error Units
ABenchmarkTest.testStringAdd thrpt 3 14.422 �� 8.952 ops/s
ABenchmarkTest.testStringBuffer thrpt 3 11.555 �� 3.352 ops/s
ABenchmarkTest.testStringBuilder thrpt 3 11.694 �� 2.872 ops/s
是近了,但是没查什么,果然 它们的差距是在 内存的消耗上,StringBuilder 毕竟不用创建多个 char[] 数组,人家就是在一个变量上 添加,确实省内存
不过拼接的少的 还是 直接用String 比较靠谱,但是String 会生成多个char[] 数组,导致内存浪费
总结:
- 如果 拼接的少 直接用String,反正人家快,但是如果项目访问量高,char[] 数组的内存浪费比较多
- 拼接多 / 或者你做的系统 访问量比较高 就用StringBuilder
有的时候 看到 人家的代码
if(log.isDebugEnabled()){
log.debug("req:" + "xxx");//虽然现在大部分都是 占位符了 哈哈
}
JMH 使用
@BenchMarkMode 设置基准测试的模式 【方法或者类】
设置运行基准测试的模式,可以选择放在方法上面,只对该方法生效,在BenchMarkMode内部设置Mode的模式
Mode.Throughput : 吞吐量模式,获得单位时间的操作数量,连续运行@BenchMark的方法,计算所有的工作线程的总吞吐量 Mode.AverageTime: 平均时间模式, 获得每次操作的平均时间,计算所有工作线程的平均时间 Mode.SimpleTime: 时间采样模式, 对每一个操作函数的时间进行采样,连续运行@BenchMark的函数,随机抽取运行所需要的时间 Mode.SingleShotTime: 单次触发模式, 测试单次操作的时间,连续运行@BenchMark函数,只运行一次并计算时间: 该模式只是运行一次@BenchMark函数,所以需要预热, 如果基准数值小,使用SimpleTime模式采样 Mode.All : 无模式,采用所有的基准模式,效果最好
@OutPutTimeUnit 报告结果的默认时间单位【类、方法】
可以放在类或者方法上面,设置测试的结果的显示的时间的单位,可以是哦那个java.util.current.TimeUnit进行设置
@OutPutTimeUnit(TimeUnit.MILLSECOUNDS)
@Warmup 预热,设置具体的配置参数如次数,时间等
JVM进程启动时,类加载器将所需要的所有类加载入内存,Bootstrap Class 核心类库,比如JRE、lib等; Extension Class 由相关的ExtClassLoader加载, Application Class 由AppClassLoader负责加载
类加载过程完毕后,所有类会进入JVM cache中,但是其他与JVM启动无关类没有加载、懒加载,当应用的第一个请求到来(比如controller的一个处理器),会触发相关类第一次加载,这个过程比较耗时, 对于低延迟应用必须要避免
采用特定的策略处理加载逻辑,保证第一次请求的快速响应,称为JVM预热
设置具体的预热的参数
iterations: 预热的迭代次数 Time: 预热的时间 timeUnit: 预热的时间单位 batchSize: 每个操作的基准方法的调用次数 (batch 一批) @Measurement 类似预热,但是设置的是测量时的 测量的参数和上面的预热的参数相同
@Fork 整体测试几次
就像之前的在main函数中设置Options中设置fork的参数,之前通过new Runner 配置参数进行run
@State 设置配置对象的作用域,定义线程之间的共享程度
可以设置测试状态对象的多线程的共享程度
Scope.Benchmark: 基准状态范围, 基准作用域: 相同类型的所有实例在所有工作线程之间共享; ---- Spring多为无状态单例Bean,可以直接所有的线程共享 此状态上面的对象的SetUp方法和TearDown方法都是一个工作线程执行,每个级别一次,没有其他线程可以操作状态对象 Scope.Group: 组状态范围、组作用域: 相同类型的所有实例在同一组中的所有的线程之间共享,每一个线程组都将提供自己的状态对象 【组内共享,组间隔离】 该状态对象上面的SetUp方法和TearDown由一个组线程执行 Scope.Thread: 线程状态范围,线程作用域: 相同类型的所有实例都不同(不是单例的),在同一个基准中注入了多个状态对象,此状态的SetUp和TearDown方法由单个工作线程独占执行 @Setup 线程执行前的配置函数、初始化 该注解只能在配置函数上面,Setup方法只由一个可以访问State对象的线程执行,一般就是一个特定的工作线程执行,如果状态共享,那么就可能由不同的线程执行(Thread 作用域)
@TearDown 测试后处理操作 【方法】
放置在方法上面进行测试后处理操作,一般测试后清理资源
@BenchMark 标记测试基准 【方法】
放在方法上面表明测试的基准
总结
这是测试性能的,你可能想问 内存怎么看,IDEA 有一个 profile 插件 可能看内存的区别
有的时候 你想看堆中有什么,栈还存在不,不想根据理论推,想实际看看,阿里 arthas 和 jstat 等jvm命令 可以满足
这里其实就有了 性能 和 内存的测试办法,之后就用这个当作证据
但是也不是万能的 ,比如 JIT优化,或者 走了缓存等等 可能会导致你的测试结果不对,也需要注意,没有银弹