Java 中的五個代碼性能提升技巧,最高提升近10倍
這篇文章介紹幾個 Java 開發(fā)中可以進(jìn)行性能優(yōu)化的小技巧,雖然大多數(shù)情況下極致優(yōu)化代碼是沒有必要的,但是作為一名技術(shù)開發(fā)者,我們還是想追求代碼的更小、更快,更強(qiáng)。如果哪天你發(fā)現(xiàn)程序的運(yùn)行速度不盡人意,可能會想到這篇文章。
提示:我們不應(yīng)該為了優(yōu)化而優(yōu)化,這有時會增加代碼的復(fù)雜度。
這篇文章中的代碼都在以下環(huán)境中進(jìn)行性能測試。
- JMH version: 1.33(Java 基準(zhǔn)測試框架)
- VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724
通過這篇文章的測試,將發(fā)現(xiàn)以下幾個操作的性能差異。
- 預(yù)先分配 HashMap 的大小,提高 1/4 的性能。
- 優(yōu)化 HashMap 的 key,性能相差 9.5 倍。
- 不使用 Enum.values() 遍歷,Spring 也曾如此優(yōu)化。
- 使用 Enum 代替 String 常量,性能高出 1.5 倍。
- 使用高版本 JDK,基礎(chǔ)操作有 2-5 倍性能差異。
預(yù)先分配 HashMap 的大小
HashMap 是 Java 中最為常用的集合之一,大多數(shù)的操作速度都非常快,但是 HashMap 在調(diào)整自身的容量大小時是很慢且難以自動優(yōu)化,因此我們在定義一個 HashMap 之前,應(yīng)該盡可能的給出它的容量大小。給出 size 值時要考慮負(fù)載因子,HashMap 默認(rèn)負(fù)載因子是 0.75,也就是要設(shè)置的 size 值要除于 0.75。
相關(guān)文章:HashMap 源碼分析解讀
下面使用 JMH 進(jìn)行基準(zhǔn)測試,測試分別向初始容量為 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,負(fù)責(zé)因子 0.75,即最多插入 12 個元素,再插入時就要進(jìn)行擴(kuò)容,所以插入 14 個元素過程中需要擴(kuò)容一次,但是如果 HashMap 初始化時就給了 32 容量,那么最多可以承載 32 * 0.75 = 24 個元素,所以插入 14 個元素時是不需要擴(kuò)容操作的。
- # 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 的性能差異了。
優(yōu)化 HashMap 的 key
如果 HashMap 的 key 值需要用到多個 String 字符串時,把字符串作為某個類屬性,然后使用這個類的實(shí)例作為 key 會比使用字符串拼接效率更高。
下面測試使用兩個字符串拼接作為 key,和把兩個字符串作為 MutablePair 類的屬性引用,然后使用 MutablePair 對象作為 key 的運(yùn)行效率差異。
- /**
- * @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])));
- }
- }
- }
測試結(jié)果:
- # 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 倍。
不使用 Enum.values() 遍歷
我們通常會使用 Enum.values() 進(jìn)行枚舉類遍歷,但是這樣每次調(diào)用都會分配枚舉類值數(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());
- }
- }
- }
運(yùn)行結(jié)果
- # 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() 的效率差異很小,其實(shí)在某些調(diào)用頻率很高的場景下是有很大區(qū)別的,在 Spring 框架中,曾使用 Enum.values() 這種方式在每次響應(yīng)時遍歷 HTTP 狀態(tài)碼枚舉類,這在請求量大時造成了不必要的性能開銷,后來進(jìn)行了 values() 緩存優(yōu)化。
下面是這次提交的截圖:
Spring Enum.values 改動
使用 Enum 代替 String 常量
使用 Enum 枚舉類代替 String 常量有明顯的好處,枚舉類強(qiáng)制驗(yàn)證,不會出錯,同時使用枚舉類的效率也更高。即使作為 Map 的 key 值來看,雖然 HashMap 的速度已經(jīng)很快了,但是使用 EnumMap 的速度可以更快。
提示:不要為了優(yōu)化而優(yōu)化,這會增加代碼的復(fù)雜度。
下面測試使用使用 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;
- /** 隨機(jī)數(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));
- }
- }
- }
運(yùn)行結(jié)果:
- # JMH version: 1.33
- # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724
- Benchmark 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ù)實(shí)際情況考慮是否使用 EnumMap 和 EnumSet。
使用高版本 JDK
String 類應(yīng)該是 Java 中使用頻率最高的類了,但是 Java 8 中的 String 實(shí)現(xiàn)相比高版本 JDK ,則占用空間更多,性能更低。
下面測試 String 轉(zhuǎn) bytes 和 bytes 轉(zhuǎn) 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));
- }
- }
- }
測試結(jié)果:
- # JMH version: 1.33
- # VM version: JDK 1.8.0_151, Java HotSpot(TM) 64-Bit Server VM, 25.151-b12
- Benchmark (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-2724
- Benchmark (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 轉(zhuǎn) String 操作上,Java 17 的性能是 Java 8 的 2.5 倍左右,而 String 轉(zhuǎn) bytes 操作,Java 17 的性能是 Java 8 的 5 倍。關(guān)于字符串的操作非?;A(chǔ),隨處可見,可見高版本的優(yōu)勢十分明顯。