Java 性能难题
1 2 3 4
| long start = System.currentTimeMillis();
long end = System.currentTimeMillis(); System.out.println(start - end);
|
上面是我们最常见的性能测试,这样的测试结果真的准确吗?答案是否定的,它有下面几个问题:
- 时间精度问题,本身获取到的时间戳就是存在误差的,它和操作系统有关。
- JVM 在运行时会进行代码预热,越跑越快。因为类需要装载、需要准备操作。
- JVM 会在各个阶段都有可能对你的代码进行优化处理。
- 资源回收的不确定性,可能运行很快,回收很慢。
带着这些问题,突然发现进行一次严格的基准测试的难度大大增加。
JMH 介绍
为了解决上面的性能难题,我们可以使用 JMH,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。使用 JMH 可以让你方便快速的进行一次严格的代码基准测试,并且有多种测试模式,多种测试维度可供选择;而且使用简单、增加注解便可启动测试。
JMH 使用
依赖引入
JMH 的使用需要引入所需的 maven 依赖
1 2 3 4 5 6 7 8 9 10 11
| <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> <scope>provided</scope> </dependency>
|
快速测试
JMH 测试:使用加号拼接字符串和使用 StringBuilder 的 append 方法拼接字符串时的速度如何,每次拼接1000个数字进行平均速度比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| @BenchmarkMode(Mode.AverageTime) @State(Scope.Thread) @Fork(1) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 3) @Measurement(iterations = 5) public class JmhHello { String string = ""; StringBuilder stringBuilder = new StringBuilder(); @Benchmark public String stringAdd() { for (int i = 0; i < 1000; i++) { string = string + i; } return string; } @Benchmark public String stringBuilderAppend() { for (int i = 0; i < 1000; i++) { stringBuilder.append(i); } return stringBuilder.toString(); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(JmhHello.class.getSimpleName()) .build(); new Runner(opt).run(); } }
|
可以看到 stringAdd() 方法平均耗时 230.424ms,而 stringBuilerAppend 方法的平均耗时只有 8.847ms,符合结论。

注解说明
@BenchmarkMode(Mode.AverageTime) :表示统计平均响应时间,不仅可以用在类上,也可用在测试方法上。
除此之外还可以取值:
- Throughput:统计单位时间内可以对方法测试多少次。
- SampleTime:统计每个响应时间范围内的响应次数,比如 0-1ms,3次;1-2ms,5次。
- SingleShotTime:跳过预热阶段,直接进行一次微基准测试。
**@State(Scope.Thread)**:每个进行基准测试的线程都会独享一个对象示例。
除此之外还能取值:
- Benchmark:多线程共享一个示例。
- Group:线程组共享一个示例,在测试方法上使用 @Group 设置线程组。
**@Fork(1)**:表示开启一个线程进行测试。
**@OutputTimeUnit(TimeUnit.MILLISECONDS)**:输出的时间单位,这里写的是毫秒。
@Warmup(iterations = 3):微基准测试前进行三次预热执行,也可用在测试方法上。
@Measurement(iterations = 5):进行 5 次微基准测试,也可用在测试方法上。
在两个测试方法上只使用了一个注解 @Benchmark,这个注解表示这个方法是要进行基准测试的方法,它类似于 Junit 中的 @Test 注解。上面还提到某些注解还可以用到测试方法上,也就是使用了 @Benchmark 的方法之上,如果类上和测试方法同时存在注解,会以方法上的注解为准。
测试各序列化技术性能
各序列化技术介绍
1、JDK 原生序列化技术:Jdk 序列化技术是一项早期就带有的序列化技术,可以将对象转换成字节数组流,供网络之间发送数据使用,但是这类序列化技术对于跨语言的兼容性并不足够友好。而且在生成的字节码流方面也并不是那么精简。在早期市面上没有那么多序列化框架出现的时候会比较受众,但是随着序列化技术的日益丰富,开发人员们也开始在逐渐替换这项技术。
2、Hessian 序列化技术:hessian是一款比Jdk序列化技术更为优秀的一款序列化框架,它能支持多款语言之间进行序列化操作,同时产生的码流也比Jdk的码流要小许多。
3、Kryo 序列化技术:Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用,其性能要比Hessian更加优秀。
4、FastJSON 序列化技术:FastJson是一款通过文本转换思路实现序列化效果的技术框架。
性能测试
在评估不同的序列化技术优劣的时候,我们通常会需要考虑这项技术在实际落地中的吞吐量,比较具有代表性的两个指标为:
实践出真知,接下来尝试对上述的几项序列化技术进行性能比对的测试。
序列化后字节流大小的比较
下面的代码使用各个序列化技术将 User 对象序列化并且反序列化,最后打印序列化结果的字节大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
|
public class SerializeByteSizeCompareTest { private static User buildUserDefault() { User user = new User(); user.setAge(11); user.setAddress("深圳市南山区"); user.setBankNo(12897873624813L); user.setSex(1); user.setId(10001); user.setIdCardNo("440308781129381222"); user.setRemark("备注信息字段"); user.setUsername("ddd-user-name"); return user; } public void jdkSerializeSizeTest() throws IOException { Serializer serializeFactory = new JdkSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); System.out.println("jdk's size is " + result.length); } public void hessianSerializeSizeTest() throws IOException { Serializer serializeFactory = new HessianSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); System.out.println("hessian's size is " + result.length); } public void fastJsonSerializeSizeTest() throws IOException { Serializer serializeFactory = new FastJsonSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); System.out.println("fastJson's size is " + result.length); } public void kryoSerializeSizeTest() throws IOException { Serializer serializeFactory = new KryoSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); System.out.println("kryo's size is " + result.length); } public static void main(String[] args) throws IOException { SerializeByteSizeCompareTest serializeByteSizeCompareTest = new SerializeByteSizeCompareTest(); serializeByteSizeCompareTest.fastJsonSerializeSizeTest(); serializeByteSizeCompareTest.jdkSerializeSizeTest(); serializeByteSizeCompareTest.kryoSerializeSizeTest(); serializeByteSizeCompareTest.hessianSerializeSizeTest(); } }
|

通过结果可以看出 kryo 序列化技术产生的代流要小一些,而 JDK 序列化技术产生的最大。
序列化处理速度的性能基准测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| @BenchmarkMode(Mode.AverageTime) @State(Scope.Thread) @Fork(1) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 2) @Measurement(iterations = 2) public class SerializeCompareTest { private static User buildUserDefault() { User user = new User(); user.setAge(11); user.setAddress("深圳市南山区"); user.setBankNo(12897873624813L); user.setSex(1); user.setId(10001); user.setIdCardNo("440308781129381222"); user.setRemark("备注信息字段"); user.setUsername("ddd-user-name"); return user; } @Benchmark public void jdkSerializeTest() throws IOException { Serializer serializeFactory = new JdkSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); } @Benchmark public void hessianSerializeTest() throws IOException { Serializer serializeFactory = new HessianSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); } @Benchmark public void fastJsonSerializeTest() throws IOException { Serializer serializeFactory = new FastJsonSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); } @Benchmark public void kryoSerializeTest() throws IOException { Serializer serializeFactory = new KryoSerializer(); User user = buildUserDefault(); byte[] result = serializeFactory.serialize(user); User deserializeUser = serializeFactory.deserialize(result, User.class); } public static void main(String[] args) throws RunnerException { Options options = new OptionsBuilder().include(SerializeCompareTest.class.getSimpleName()) .forks(1) .build(); new Runner(options).run(); } }
|
具体而言:
- **
@BenchmarkMode(Mode.AverageTime)**:设置基准测试模式为平均时间测量,即测试执行操作的平均时间
- **
@State(Scope.Thread)**:定义状态的作用范围为单线程。每个测试线程都会有该类的一个实例
- **
@Fork(1)**:设置基准测试的分叉次数为1,即测试将运行一个单独的JVM实例
- **
@OutputTimeUnit(TimeUnit.MILLISECONDS)**:设置输出的时间单位为毫秒
- **
@Warmup(iterations = 2)**:在正式计时测试之前进行2轮预热,以便JVM优化代码。
- **
@Measurement(iterations = 2)**:实际测量时进行2轮测试
从结果可以看出,FastJSON 序列化技术效果最好,JDK 原生序列化技术效果最差