我們一起聊聊對象與 Map 轉(zhuǎn)換性能優(yōu)化方案
前言
有粉絲提到每次都需要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) 對這段代碼進(jìn)行基準(zhǔn)測試,以便大家對其性能有一個直觀的了解。
圖片
上圖是一個典型的JMH程序執(zhí)行的內(nèi)容。通過開啟多個進(jìn)程,多個線程,首先執(zhí)行預(yù)熱,然后執(zhí)行迭代,最后匯總所有的測試數(shù)據(jù)進(jìn)行分析。在執(zhí)行前后,還可以根據(jù)粒度處理一些前置和后置操作。
JMH 是 Java 語言的微基準(zhǔn)測試框架,用于準(zhǔn)確、可靠地測量和評估Java代碼的性能。它是由OpenJDK團隊開發(fā)的,專門針對Java應(yīng)用程序的性能測試和基準(zhǔn)測試。通過JMH 可以對多個方法的性能進(jìn)行定量分析。比如,當(dāng)要知道執(zhí)行一個函數(shù)需要多少時間,或者當(dāng)對一個算法有多種不同實現(xiàn)時,需要選取性能最好的那個。
依賴引入
<!-- JMH核心代碼 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<!-- JMH注解相關(guān)依賴 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
注解說明
BenchmarkMode
名稱 | 解釋 | 單位 |
Mode.Throughput | operations per unit of time.(單位時間執(zhí)行的次數(shù)) | ops/time |
Mode.AverageTime | average time per per operation(每個方法執(zhí)行的平均時間) | time/op |
Mode.SampleTime | samples the time for each operation.(每個方法執(zhí)行的時間) | time |
Mode.SingleShotTime | measures the time for a single operation.(單個的執(zhí)行時間) | |
All | all the benchmark modes. (上面所有都執(zhí)行一次) |
OutputTimeUnit
統(tǒng)計的時間單位
Warmup、Measurement
名稱 | 解釋 |
iterations | 預(yù)熱次數(shù) |
time | 預(yù)熱時間 |
timeUnit | 預(yù)熱時間單位 |
batchSize | 同時預(yù)熱 |
State
名稱 | 解釋 |
Benchmark | 所有測試共享線程。做多線程的時候使用 |
Group | 每一組中共享線程 |
Thread | 每一個方法或者類共享線程 |
Fork
進(jìn)行次數(shù),如果 fork 數(shù)是2的話,則 JMH 會 fork 出兩個進(jìn)程來進(jìn)行測試
Benchmark
表示該方法是需要進(jìn)行 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();
}
測試結(jié)果:
# 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
通過測試結(jié)果可以看出,在實現(xiàn)對象轉(zhuǎn)map時每秒可以完成8萬多次,而實現(xiàn)map轉(zhuǎn)對象轉(zhuǎn)每秒僅可完成3.2萬次。
如何優(yōu)化
我們都知道在創(chuàng)建工具類時,應(yīng)將其設(shè)計為單例模式,保證在整個系統(tǒng)中僅有一個實例,從而避免因頻繁創(chuàng)建對象而帶來的成本。
@Getter
public enum ObjectMapperInstance {
INSTANCE;
private final ObjectMapper objectMapper = new ObjectMapper();
ObjectMapperInstance() {
}
}
枚舉類型的單例實現(xiàn)天然線程安全,并且可以抵御反射攻擊。
再次測試驗證
# 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 設(shè)計為單例模式,我們可以顯著提升性能。在整個項目中只需創(chuàng)建一個 ObjectMapper 實例,避免了每次使用時重新創(chuàng)建對象所帶來的開銷。這種方法不僅提高了性能,還簡化了代碼管理和減少了內(nèi)存消耗。
同樣的原則可以應(yīng)用于其他工具類或頻繁使用的對象。例如,數(shù)據(jù)庫連接池、緩存客戶端、日志記錄器等,都可以通過單例模式來優(yōu)化性能和資源管理。確保這些組件在整個應(yīng)用生命周期內(nèi)只創(chuàng)建一次,可以最大化其效用并減少不必要的資源消耗。
總之,將頻繁使用的工具類設(shè)計為單例模式是一種良好的編程實踐,它不僅提升了性能,還增強了代碼的可維護性和可讀性。