前言
有粉丝提到每次都需要new一个ObjectMapper对象,并且提到性能压测。
Person person = new Person();
person.setAge(18);
person.setOpenid("123456");
person.setName("一安");
person.setSubName("公众号");
System.out.println(bean2Map(person));
public static Map<String, Object> bean2Map(Object object) {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.convertValue(object, new TypeReference<Map<String, Object>>() {
});
}
改造
首先,我们将使用 Java Microbenchmark Harness (JMH) 对这段代码进行基准测试,以便大家对其性能有一个直观的了解。
图片
上图是一个典型的JMH程序执行的内容。通过开启多个进程,多个线程,首先执行预热,然后执行迭代,最后汇总所有的测试数据进行分析。在执行前后,还可以根据粒度处理一些前置和后置操作。
JMH 是 Java 语言的微基准测试框架,用于准确、可靠地测量和评估Java代码的性能。它是由OpenJDK团队开发的,专门针对Java应用程序的性能测试和基准测试。通过JMH 可以对多个方法的性能进行定量分析。比如,当要知道执行一个函数需要多少时间,或者当对一个算法有多种不同实现时,需要选取性能最好的那个。
依赖引入
<!-- JMH核心代码 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<!-- JMH注解相关依赖 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
注解说明
BenchmarkMode
名称 | 解释 | 单位 |
Mode.Throughput | operations per unit of time.(单位时间执行的次数) | ops/time |
Mode.AverageTime | average time per per operation(每个方法执行的平均时间) | time/op |
Mode.SampleTime | samples the time for each operation.(每个方法执行的时间) | time |
Mode.SingleShotTime | measures the time for a single operation.(单个的执行时间) | |
All | all the benchmark modes. (上面所有都执行一次) |
OutputTimeUnit
统计的时间单位
Warmup、Measurement
名称 | 解释 |
iterations | 预热次数 |
time | 预热时间 |
timeUnit | 预热时间单位 |
batchSize | 同时预热 |
State
名称 | 解释 |
Benchmark | 所有测试共享线程。做多线程的时候使用 |
Group | 每一组中共享线程 |
Thread | 每一个方法或者类共享线程 |
Fork
进行次数,如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试
Benchmark
表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似
测试验证
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Thread)
@Fork(1)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 3, time = 1)
public class JsonJMHTest {
@Benchmark
public static Map<String, Object> bean2Map() {
Person person = new Person();
person.setAge(18);
person.setOpenid("123456");
person.setName("一安");
person.setSubName("公众号");
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.convertValue(person, new TypeReference<Map<String, Object>>() {
});
}
@Benchmark
public static <T> T map2Bean() {
Map<String, Object> map = new HashMap();
map.put("age", 18);
map.put("openid", "123456");
map.put("name", "一安");
map.put("subName", "公众号");
ObjectMapper objectMapper = new ObjectMapper();
return (T) objectMapper.convertValue(map, Person.class);
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JsonJMHTest.class.getSimpleName())
.build();
new Runner(opt).run();
}
测试结果:
# Fork: 1 of 1
# Warmup Iteration 1: 4121.577 ops/s
# Warmup Iteration 2: 10599.791 ops/s
# Warmup Iteration 3: 1945.716 ops/s
# Warmup Iteration 4: 7284.198 ops/s
# Warmup Iteration 5: 8161.620 ops/s
Iteration 1: 841.544 ops/s
Iteration 2: 25483.108 ops/s
Iteration 3: 70902.482 ops/s
Result "org.example.JsonJMHTest.map2Bean":
32409.045 ±(99.9%) 648386.677 ops/s [Average]
(min, avg, max) = (841.544, 32409.045, 70902.482), stdev = 35540.262
CI (99.9%): [≈ 0, 680795.722] (assumes normal distribution)
# Run complete. Total time: 00:00:22
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
JsonJMHTest.bean2Map thrpt 3 80269.397 ± 61739.014 ops/s
JsonJMHTest.map2Bean thrpt 3 32409.045 ± 648386.677 ops/s
通过测试结果可以看出,在实现对象转map时每秒可以完成8万多次,而实现map转对象转每秒仅可完成3.2万次。
如何优化
我们都知道在创建工具类时,应将其设计为单例模式,保证在整个系统中仅有一个实例,从而避免因频繁创建对象而带来的成本。
@Getter
public enum ObjectMapperInstance {
INSTANCE;
private final ObjectMapper objectMapper = new ObjectMapper();
ObjectMapperInstance() {
}
}
枚举类型的单例实现天然线程安全,并且可以抵御反射攻击。
再次测试验证
# Fork: 1 of 1
# Warmup Iteration 1: 916836.618 ops/s
# Warmup Iteration 2: 2057459.265 ops/s
# Warmup Iteration 3: 1992614.947 ops/s
# Warmup Iteration 4: 524763.395 ops/s
# Warmup Iteration 5: 2463816.439 ops/s
Iteration 1: 2570659.849 ops/s
Iteration 2: 2557669.589 ops/s
Iteration 3: 2548610.266 ops/s
Result "org.example.JsonJMHTest.map2Bean":
2558979.901 ±(99.9%) 202195.856 ops/s [Average]
(min, avg, max) = (2548610.266, 2558979.901, 2570659.849), stdev = 11083.037
CI (99.9%): [2356784.046, 2761175.757] (assumes normal distribution)
# Run complete. Total time: 00:00:30
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
JsonJMHTest.bean2Map thrpt 3 1107857.325 ± 19284117.404 ops/s
JsonJMHTest.map2Bean thrpt 3 2558979.901 ± 202195.856 ops/s
通过将 ObjectMapper 设计为单例模式,我们可以显著提升性能。在整个项目中只需创建一个 ObjectMapper 实例,避免了每次使用时重新创建对象所带来的开销。这种方法不仅提高了性能,还简化了代码管理和减少了内存消耗。
同样的原则可以应用于其他工具类或频繁使用的对象。例如,数据库连接池、缓存客户端、日志记录器等,都可以通过单例模式来优化性能和资源管理。确保这些组件在整个应用生命周期内只创建一次,可以最大化其效用并减少不必要的资源消耗。
总之,将频繁使用的工具类设计为单例模式是一种良好的编程实践,它不仅提升了性能,还增强了代码的可维护性和可读性。