Java?中的5個代碼性能提升技巧
前言:
提示:我們不應該為了優(yōu)化而優(yōu)化,這有時會增加代碼的復雜度。
這篇文章中的代碼都在以下環(huán)境中進行性能測試。
- JMH version: 1.33(Java 基準測試框架)
- VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724
通過這篇文章的測試,將發(fā)現(xiàn)以下幾個操作的性能差異。
- 預先分配
HashMap
的大小,提高 1/4 的性能。 - 優(yōu)化
HashMap
的key
,性能相差 9.5 倍。 - 不使用
Enum.values()
遍歷,Spring
也曾如此優(yōu)化。 - 使用 Enum 代替
String
常量,性能高出 1.5 倍。 - 使用高版本 JDK,基礎操作有 2-5 倍性能差異。
1.預先分配 HashMap 的大小
HashMap
是 Java 中最為常用的集合之一,大多數(shù)的操作速度都非???,但是 HashMap 在調整自身的容量大小時是很慢且難以自動優(yōu)化,因此我們在定義一個 HashMap 之前,應該盡可能的給出它的容量大小。給出 size 值時要考慮負載因子,HashMap
默認負載因子是 0.75,也就是要設置的 size 值要除于 0.75。
相關文章:HashMap 源碼分析解讀
下面使用 JMH 進行基準測試,測試分別向初始容量為 16 和 32 的 HashMap 中插入 14 個元素的效率。
/** * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3,time = 3) @Measurement(iterations = 5,time = 3) public class HashMapSize { @Param({"14"}) int keys; @Param({"16", "32"}) int size; @Benchmark public HashMap<Integer, Integer> getHashMap() { HashMap<Integer, Integer> map = new HashMap<>(size); for (int i = 0; i < keys; i++) { map.put(i, i); } return map; } }
HashMap
的初始容量是 16,負責因子 0.75,即最多插入 12 個元素,再插入時就要進行擴容,所以插入 14 個元素過程中需要擴容一次,但是如果 HashMap 初始化時就給了 32 容量,那么最多可以承載 32 * 0.75 = 24 個元素,所以插入 14 個元素時是不需要擴容操作的。
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark (keys) (size) Mode Cnt Score Error Units HashMapSize.getHashMap 14 16 thrpt 25 4825825.152 ± 323910.557 ops/s HashMapSize.getHashMap 14 32 thrpt 25 6556184.664 ± 711657.679 ops/s
可以看到在這次測試中,初始容量為32 的 HashMap
比初始容量為 16 的 HashMap
每秒可以多操作 26% 次,已經(jīng)有 1/4 的性能差異了。
2.優(yōu)化 HashMap 的 key
如果 HashMap
的 key 值需要用到多個 String 字符串時,把字符串作為某個類屬性,然后使用這個類的實例作為 key 會比使用字符串拼接效率更高。
下面測試使用兩個字符串拼接作為 key,和把兩個字符串作為 MutablePair 類的屬性引用,然后使用 MutablePair 對象作為 key 的運行效率差異。
/** * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) public class HashMapKey { private int size = 1024; private Map<String, Object> stringMap; private Map<Pair, Object> pairMap; private String[] prefixes; private String[] suffixes; @Setup(Level.Trial) public void setup() { prefixes = new String[size]; suffixes = new String[size]; stringMap = new HashMap<>(); pairMap = new HashMap<>(); for (int i = 0; i < size; ++i) { prefixes[i] = UUID.randomUUID().toString(); suffixes[i] = UUID.randomUUID().toString(); stringMap.put(prefixes[i] + ";" + suffixes[i], i); // use new String to avoid reference equality speeding up the equals calls pairMap.put(new MutablePair(prefixes[i], suffixes[i]), i); } } @Benchmark @OperationsPerInvocation(1024) public void stringKey(Blackhole bh) { for (int i = 0; i < prefixes.length; i++) { bh.consume(stringMap.get(prefixes[i] + ";" + suffixes[i])); } } @Benchmark @OperationsPerInvocation(1024) public void pairMap(Blackhole bh) { for (int i = 0; i < prefixes.length; i++) { bh.consume(pairMap.get(new MutablePair(prefixes[i], suffixes[i]))); } } }
測試結果:
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark Mode Cnt Score Error Units HashMapKey.pairMap thrpt 25 89295035.436 ± 6498403.173 ops/s HashMapKey.stringKey thrpt 25 9410641.728 ± 389850.653 ops/s
可以發(fā)現(xiàn)使用對象引用作為 key
的性能,是使用 String
拼接作為 key 的性能的 9.5 倍。
3.不使用 Enum.values() 遍歷
我們通常會使用 Enum.values()
進行枚舉類遍歷,但是這樣每次調用都會分配枚舉類值數(shù)量大小的數(shù)組用于操作,這里完全可以緩存起來,以減少每次內(nèi)存分配的時間和空間消耗。
/** * 枚舉類遍歷測試 * * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class EnumIteration { enum FourteenEnum { a,b,c,d,e,f,g,h,i,j,k,l,m,n; static final FourteenEnum[] VALUES; static { VALUES = values(); } } @Benchmark public void valuesEnum(Blackhole bh) { for (FourteenEnum value : FourteenEnum.values()) { bh.consume(value.ordinal()); } } @Benchmark public void enumSetEnum(Blackhole bh) { for (FourteenEnum value : EnumSet.allOf(FourteenEnum.class)) { bh.consume(value.ordinal()); } } @Benchmark public void cacheEnums(Blackhole bh) { for (FourteenEnum value : FourteenEnum.VALUES) { bh.consume(value.ordinal()); } } }
運行結果:
# JMH version: 1.33 # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 Benchmark Mode Cnt Score Error Units EnumIteration.cacheEnums thrpt 25 15623401.567 ± 2274962.772 ops/s EnumIteration.enumSetEnum thrpt 25 8597188.662 ± 610632.249 ops/s EnumIteration.valuesEnum thrpt 25 14713941.570 ± 728955.826 ops/s
很明顯使用緩存后的遍歷速度是最快的,使用 EnumSet
遍歷效率是最低的,這很好理解,數(shù)組的遍歷效率是大于哈希表的。
可能你會覺得這里使用 values() 緩存和直接使用 Enum.values()
的效率差異很小,其實在某些調用頻率很高的場景下是有很大區(qū)別的,在 Spring 框架中,曾使用 Enum.values()
這種方式在每次響應時遍歷 HTTP 狀態(tài)碼枚舉類,這在請求量大時造成了不必要的性能開銷,后來進行了 values()
緩存優(yōu)化。
下面是這次提交的截圖:
4.使用 Enum 代替 String 常量
使用 Enum
枚舉類代替 String 常量有明顯的好處,枚舉類強制驗證,不會出錯,同時使用枚舉類的效率也更高。即使作為 Map 的 key 值來看,雖然 HashMap
的速度已經(jīng)很快了,但是使用 EnumMap
的速度可以更快。
提示:不要為了優(yōu)化而優(yōu)化,這會增加代碼的復雜度。
下面測試使用使用 Enum
作為 key,和使用 String 作為 key,在 map.get
操作下的性能差異。
/** * @author https://www.wdbyte.com */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) public class EnumMapBenchmark { enum AnEnum { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; } /** 要查找的 key 的數(shù)量 */ private static int size = 10000; /** 隨機數(shù)種子 */ private static int seed = 99; @State(Scope.Benchmark) public static class EnumMapState { private EnumMap<AnEnum, String> map; private AnEnum[] values; @Setup(Level.Trial) public void setup() { map = new EnumMap<>(AnEnum.class); values = new AnEnum[size]; AnEnum[] enumValues = AnEnum.values(); SplittableRandom random = new SplittableRandom(seed); for (int i = 0; i < size; i++) { int nextInt = random.nextInt(0, Integer.MAX_VALUE); values[i] = enumValues[nextInt % enumValues.length]; } for (AnEnum value : enumValues) { map.put(value, UUID.randomUUID().toString()); } } } @State(Scope.Benchmark) public static class HashMapState{ private HashMap<String, String> map; private String[] values; @Setup(Level.Trial) public void setup() { map = new HashMap<>(); values = new String[size]; AnEnum[] enumValues = AnEnum.values(); int pos = 0; SplittableRandom random = new SplittableRandom(seed); for (int i = 0; i < size; i++) { int nextInt = random.nextInt(0, Integer.MAX_VALUE); values[i] = enumValues[nextInt % enumValues.length].toString(); } for (AnEnum value : enumValues) { map.put(value.toString(), UUID.randomUUID().toString()); } } } @Benchmark public void enumMap(EnumMapState state, Blackhole bh) { for (AnEnum value : state.values) { bh.consume(state.map.get(value)); } } @Benchmark public void hashMap(HashMapState state, Blackhole bh) { for (String value : state.values) { bh.consume(state.map.get(value)); } } }
運行結果:
# JMH version: 1.33
# VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724Benchmark????????????????? Mode? Cnt????? Score????? Error? Units
EnumMapBenchmark.enumMap? thrpt?? 25? 22159.232 ± 1268.800? ops/s
EnumMapBenchmark.hashMap? thrpt?? 25? 14528.555 ± 1323.610? ops/s
很明顯,使用 Enum
作為 key 的性能比使用 String 作為 key 的性能高出 1.5 倍。但是仍然要根據(jù)實際情況考慮是否使用 EnumMap
和 EnumSet
。
5.使用高版本 JDK
String 類應該是 Java 中使用頻率最高的類了,但是 Java 8 中的 String
實現(xiàn)相比高版本 JDK ,則占用空間更多,性能更低。
下面測試 String
轉 bytes
和 bytes 轉 String 在 Java 8 以及 Java 11 中的性能開銷。
/** * @author https://www.wdbyte.com * @date 2021/12/23 */ @State(Scope.Benchmark) @Warmup(iterations = 3, time = 3) @Measurement(iterations = 5, time = 3) public class StringInJdk { @Param({"10000"}) private int size; private String[] stringArray; private List<byte[]> byteList; @Setup(Level.Trial) public void setup() { byteList = new ArrayList<>(size); stringArray = new String[size]; for (int i = 0; i < size; i++) { String uuid = UUID.randomUUID().toString(); stringArray[i] = uuid; byteList.add(uuid.getBytes(StandardCharsets.UTF_8)); } } @Benchmark public void byteToString(Blackhole bh) { for (byte[] bytes : byteList) { bh.consume(new String(bytes, StandardCharsets.UTF_8)); } } @Benchmark public void stringToByte(Blackhole bh) { for (String s : stringArray) { bh.consume(s.getBytes(StandardCharsets.UTF_8)); } } }
測試結果:
# JMH version: 1.33
# VM version: JDK 1.8.0_151, Java HotSpot(TM) 64-Bit Server VM, 25.151-b12Benchmark???????????????? (size)?? Mode? Cnt???? Score???? Error? Units
StringInJdk.byteToString?? 10000? thrpt?? 25? 2396.713 ± 133.500? ops/s
StringInJdk.stringToByte?? 10000? thrpt?? 25? 1745.060 ±? 16.945? ops/s# JMH version: 1.33
# VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724Benchmark???????????????? (size)?? Mode? Cnt???? Score???? Error? Units
StringInJdk.byteToString?? 10000? thrpt?? 25? 5711.954 ±? 41.865? ops/s
StringInJdk.stringToByte?? 10000? thrpt?? 25? 8595.895 ± 704.004? ops/s
可以看到在 bytes
轉 String
操作上,Java 17 的性能是 Java 8 的 2.5 倍左右,而 String 轉 bytes 操作,Java 17 的性能是 Java 8 的 5 倍。關于字符串的操作非?;A,隨處可見,可見高版本的優(yōu)勢十分明顯。
到此這篇關于Java 中的5個代碼性能提升技巧的文章就介紹到這了,更多相關Java 代碼性能提升技巧內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
參考:
https://richardstartin.github.io/posts/5-java-mundane-performance-tricks
https://github.com/spring-projects/spring-framework/issues/26842
https://github.com/spring-projects/spring-framework/commit/7f1062159ee9926d5abed7cadc2b36b6b7fc242e
相關文章
SharedingSphere?自定義脫敏規(guī)則介紹
這篇文章主要介紹了SharedingSphere?自定義脫敏規(guī)則,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12java 中 System.out.println()和System.out.write()的區(qū)別
這篇文章主要介紹了 java 中 System.out.println()和System.out.write()的區(qū)別.的相關資料,需要的朋友可以參考下2017-04-04IntelliJ IDEA Java項目手動添加依賴 jar 包的方法(圖解)
這篇文章主要介紹了IntelliJ IDEA Java項目手動添加依賴 jar 包,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04關于Linux服務器配置java環(huán)境遇到的問題小結
這篇文章主要介紹了關于Linux服務器配置java環(huán)境遇到的問題小結,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12