Java 性能难题

1
2
3
4
long start = System.currentTimeMillis();
// ...
long end = System.currentTimeMillis();
System.out.println(start - end);

上面是我们最常见的性能测试,这样的测试结果真的准确吗?答案是否定的,它有下面几个问题:

  1. 时间精度问题,本身获取到的时间戳就是存在误差的,它和操作系统有关。
  2. JVM 在运行时会进行代码预热,越跑越快。因为类需要装载、需要准备操作。
  3. JVM 会在各个阶段都有可能对你的代码进行优化处理。
  4. 资源回收的不确定性,可能运行很快,回收很慢。

带着这些问题,突然发现进行一次严格的基准测试的难度大大增加。

JMH 介绍

为了解决上面的性能难题,我们可以使用 JMH,它是由 Java 虚拟机团队开发的一款用于 Java 微基准测试工具。使用 JMH 可以让你方便快速的进行一次严格的代码基准测试,并且有多种测试模式,多种测试维度可供选择;而且使用简单、增加注解便可启动测试。

JMH 使用

依赖引入

JMH 的使用需要引入所需的 maven 依赖

1
2
3
4
5
6
7
8
9
10
11
<!--jmh 基准测试 -->
<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,符合结论。

image.png

注解说明

@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
/**  
* 码流大小的对比测试
*
* @author Ethan * @version 1.0 */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();
}
}

image.png

通过结果可以看出 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 原生序列化技术效果最差