这篇文章介绍几个 Java 开发中可以进行性能优化的小技巧,虽然大多数情况下极致优化代码是没有必要的,但是作为一名技术开发者,我们还是想追求代码的更小、更快,更强。如果哪天你发现程序的运行速度不尽人意,可能会想到这篇文章。

提示:我们不应该为了优化而优化,这有时会增加代码的复杂度。

这篇文章中的代码都在以下环境中进行性能测试。

  •  JMH version: 1.33(Java 基准测试框架)

  •  VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724

通过这篇文章的测试,将发现以下几个操作的性能差异。

  1.   预先分配 HashMap 的大小,提高 1/4 的性能。

  2.   优化 HashMap 的 key,性能相差 9.5 倍。

  3.   不使用  Enum.values() 遍历,Spring 也曾如此优化。

  4.   使用 Enum 代替 String 常量,性能高出 1.5 倍。

  5.   使用高版本 JDK,基础操作有 2-5 倍性能差异。

预先分配 HashMap 的大小

HashMap 是 Java 中最为常用的集合之一,大多数的操作速度都非常快,但是 HashMap 在调整自身的容量大小时是很慢且难以自动优化,因此我们在定义一个 HashMap 之前,应该尽可能的给出它的容量大小。给出 size 值时要考虑负载因子,HashMap 默认负载因子是 0.75,也就是要设置的 size 值要除于 0.75。

相关文章:HashMap 源码分析解读

下面使用 JMH 进行基准测试,测试分别向初始容量为 16 和 32 的 HashMap 中插入 14 个元素的效率。

  1. /**  

  2.  * @author https://www.wdbyte.com  

  3.  */  

  4. @State(Scope.Benchmark)  

  5. @Warmup(iterations = 3,time = 3 

  6. @Measurement(iterations = 5,time = 3 

  7. public class HashMapSize {  

  8.     @Param({"14"})  

  9.     int keys;  

  10.     @Param({"16", "32"})  

  11.     int size;  

  12.     @Benchmark  

  13.     public HashMap<Integer, Integer> getHashMap() {  

  14.         HashMap<Integer, Integer> map = new HashMap<>(size); 

  15.          for (int i = 0; i < keys; i++) {  

  16.             map.put(i, i);  

  17.         }  

  18.         return map; 

  19.    }  

HashMap 的初始容量是 16,负责因子 0.75,即最多插入 12 个元素,再插入时就要进行扩容,所以插入 14 个元素过程中需要扩容一次,但是如果 HashMap 初始化时就给了 32 容量,那么最多可以承载 32 * 0.75 = 24 个元素,所以插入 14 个元素时是不需要扩容操作的。

  1. # JMH version: 1.33  

  2. # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724  

  3. Benchmark               (keys)  (size)   Mode  Cnt        Score        Error  Units  

  4. HashMapSize.getHashMap      14      16  thrpt   25  4825825.152 ± 323910.557  ops/s  

  5. HashMapSize.getHashMap      14      32  thrpt   25  6556184.664 ± 711657.679  ops/s 

可以看到在这次测试中,初始容量为32 的 HashMap 比初始容量为 16 的 HashMap 每秒可以多操作 26% 次,已经有 1/4 的性能差异了。

 优化 HashMap 的 key

如果 HashMap 的 key 值需要用到多个 String 字符串时,把字符串作为某个类属性,然后使用这个类的实例作为 key 会比使用字符串拼接效率更高。

下面测试使用两个字符串拼接作为 key,和把两个字符串作为 MutablePair 类的属性引用,然后使用 MutablePair 对象作为 key 的运行效率差异。

  1. /**  

  2.  * @author https://www.wdbyte.com  

  3.  */  

  4. @State(Scope.Benchmark)  

  5. @Warmup(iterations = 3time = 3 

  6. @Measurement(iterations = 5time = 3 

  7. public class HashMapKey {  

  8.     private int size = 1024 

  9.     private Map<String, Object> stringMap;  

  10.     private Map<Pair, Object> pairMap;  

  11.     private String[] prefixes;  

  12.     private String[] suffixes;  

  13.     @Setup(Level.Trial)  

  14.     public void setup() {  

  15.         prefixes = new String[size];  

  16.         suffixes = new String[size];  

  17.         stringMap = new HashMap<>();  

  18.         pairMap = new HashMap<>();  

  19.         for (int i = 0; i < size; ++i) {  

  20.             prefixes[i] = UUID.randomUUID().toString();  

  21.             suffixes[i] = UUID.randomUUID().toString();  

  22.             stringMap.put(prefixes[i] + ";" + suffixes[i], i);  

  23.             // use new String to avoid reference equality speeding up the equals calls  

  24.             pairMap.put(new MutablePair(prefixes[i], suffixes[i]), i);  

  25.         }  

  26.     }  

  27.     @Benchmark  

  28.     @OperationsPerInvocation(1024)  

  29.     public void stringKey(Blackhole bh) {  

  30.         for (int i = 0; i < prefixes.length; i++) {  

  31.             bh.consume(stringMap.get(prefixes[i] + ";" + suffixes[i]));  

  32.         }  

  33.     }  

  34.     @Benchmark  

  35.     @OperationsPerInvocation(1024)  

  36.     public void pairMap(Blackhole bh) {  

  37.         for (int i = 0; i < prefixes.length; i++) {  

  38.             bh.consume(pairMap.get(new MutablePair(prefixes[i], suffixes[i])));  

  39.         }  

  40.     }  


测试结果:

  1. # JMH version: 1.33  

  2. # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724  

  3. Benchmark              Mode  Cnt         Score         Error  Units  

  4. HashMapKey.pairMap    thrpt   25  89295035.436 ± 6498403.173  ops/s  

  5. HashMapKey.stringKey  thrpt   25   9410641.728 ±  389850.653  ops/s 

可以发现使用对象引用作为 key 的性能,是使用 String 拼接作为 key 的性能的 9.5 倍。

不使用  Enum.values() 遍历

我们通常会使用  Enum.values() 进行枚举类遍历,但是这样每次调用都会分配枚举类值数量大小的数组用于操作,这里完全可以缓存起来,以减少每次内存分配的时间和空间消耗。

  1. /**  

  2.  * 枚举类遍历测试  

  3.  *  

  4.  * @author https://www.wdbyte.com  

  5.  */  

  6. @State(Scope.Benchmark)  

  7. @Warmup(iterations = 3time = 3 

  8. @Measurement(iterations = 5time = 3 

  9. @BenchmarkMode(Mode.AverageTime)  

  10. @OutputTimeUnit(TimeUnit.MILLISECONDS)  

  11. public class EnumIteration {  

  12.     enum FourteenEnum {  

  13.         a,b,c,d,e,f,g,h,i,j,k,l,m,n;  

  14.         static final FourteenEnum[] VALUES;  

  15.         static {  

  16.             VALUES = values();  

  17.         }  

  18.     }  

  19.     @Benchmark  

  20.     public void valuesEnum(Blackhole bh) {  

  21.         for (FourteenEnum value : FourteenEnum.values()) {  

  22.             bh.consume(value.ordinal());  

  23.         }  

  24.     }  

  25.     @Benchmark  

  26.     public void enumSetEnum(Blackhole bh) { 

  27.         for (FourteenEnum value : EnumSet.allOf(FourteenEnum.class)) {  

  28.             bh.consume(value.ordinal());  

  29.         }  

  30.     }  

  31.     @Benchmark  

  32.     public void cacheEnums(Blackhole bh) {  

  33.         for (FourteenEnum value : FourteenEnum.VALUES) {  

  34.             bh.consume(value.ordinal());  

  35.         }  

  36.     }  

运行结果

  1. # JMH version: 1.33  

  2. # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724  

  3. Benchmark                   Mode  Cnt         Score         Error  Units  

  4. EnumIteration.cacheEnums   thrpt   25  15623401.567 ± 2274962.772  ops/s  

  5. EnumIteration.enumSetEnum  thrpt   25   8597188.662 ±  610632.249  ops/s  

  6. EnumIteration.valuesEnum   thrpt   25  14713941.570 ±  728955.826  ops/s 

很明显使用缓存后的遍历速度是最快的,使用 EnumSet 遍历效率是最低的,这很好理解,数组的遍历效率是大于哈希表的。

可能你会觉得这里使用 values() 缓存和直接使用 Enum.values() 的效率差异很小,其实在某些调用频率很高的场景下是有很大区别的,在 Spring 框架中,曾使用 Enum.values() 这种方式在每次响应时遍历 HTTP 状态码枚举类,这在请求量大时造成了不必要的性能开销,后来进行了 values() 缓存优化。

下面是这次提交的截图:

Spring Enum.values 改动

使用 Enum 代替 String 常量

使用 Enum 枚举类代替 String 常量有明显的好处,枚举类强制验证,不会出错,同时使用枚举类的效率也更高。即使作为 Map 的 key 值来看,虽然 HashMap 的速度已经很快了,但是使用 EnumMap 的速度可以更快。

提示:不要为了优化而优化,这会增加代码的复杂度。

下面测试使用使用 Enum 作为 key,和使用 String 作为 key,在 map.get 操作下的性能差异。

  1. /**  

  2.  * @author https://www.wdbyte.com  

  3.  */  

  4. @State(Scope.Benchmark)  

  5. @Warmup(iterations = 3time = 3 

  6. @Measurement(iterations = 5time = 3 

  7. public class EnumMapBenchmark {  

  8.     enum AnEnum {  

  9.         a, b, c, d, e, f, g,  

  10.         h, i, j, k, l, m, n,  

  11.         o, p, q,    r, s, t,  

  12.         u, v, w,    x, y, z;  

  13.     }  

  14.     /** 要查找的 key 的数量 */  

  15.     private static int size = 10000 

  16.     /** 随机数种子 */  

  17.     private static int seed = 99 

  18.     @State(Scope.Benchmark)  

  19.     public static class EnumMapState {  

  20.         private EnumMap<AnEnum, String> map;  

  21.         private AnEnum[] values;  

  22.         @Setup(Level.Trial)  

  23.         public void setup() {  

  24.             map = new EnumMap<>(AnEnum.class);  

  25.             values = new AnEnum[size];  

  26.             AnEnum[] enumValues = AnEnum.values();  

  27.             SplittableRandom random = new SplittableRandom(seed);  

  28.             for (int i = 0; i < size; i++) {  

  29.                 int nextInt = random.nextInt(0, Integer.MAX_VALUE);  

  30.                 values[i] = enumValues[nextInt % enumValues.length];  

  31.             }  

  32.             for (AnEnum value : enumValues) {  

  33.                 map.put(value, UUID.randomUUID().toString());  

  34.             }  

  35.         }  

  36.     }  

  37.     @State(Scope.Benchmark)  

  38.     public static class HashMapState{  

  39.         private HashMap<String, String> map;  

  40.         private String[] values;  

  41.         @Setup(Level.Trial)  

  42.         public void setup() {  

  43.             map = new HashMap<>();  

  44.             values = new String[size];  

  45.             AnEnum[] enumValues = AnEnum.values();  

  46.             int pos = 0 

  47.             SplittableRandom random = new SplittableRandom(seed);  

  48.             for (int i = 0; i < size; i++) {  

  49.                 int nextInt = random.nextInt(0, Integer.MAX_VALUE);  

  50.                 values[i] = enumValues[nextInt % enumValues.length].toString();  

  51.             }  

  52.             for (AnEnum value : enumValues) {  

  53.                 map.put(value.toString(), UUID.randomUUID().toString());  

  54.             }  

  55.         }  

  56.     }  

  57.     @Benchmark  

  58.     public void enumMap(EnumMapState state, Blackhole bh) {  

  59.         for (AnEnum value : state.values) {  

  60.             bh.consume(state.map.get(value));  

  61.         }  

  62.     }  

  63.     @Benchmark  

  64.     public void hashMap(HashMapState state, Blackhole bh) {  

  65.         for (String value : state.values) {  

  66.             bh.consume(state.map.get(value));  

  67.         }  

  68.     }  

运行结果:

  1. # JMH version: 1.33  

  2. # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724  

  3. Benchmark                  Mode  Cnt      Score      Error  Units  

  4. EnumMapBenchmark.enumMap  thrpt   25  22159.232 ± 1268.800  ops/s  

  5. EnumMapBenchmark.hashMap  thrpt   25  14528.555 ± 1323.610  ops/s 

很明显,使用 Enum 作为 key 的性能比使用 String 作为 key 的性能高出 1.5 倍。但是仍然要根据实际情况考虑是否使用 EnumMap 和 EnumSet。

 使用高版本 JDK

String 类应该是 Java 中使用频率最高的类了,但是 Java 8 中的  String 实现相比高版本 JDK ,则占用空间更多,性能更低。

下面测试 String 转 bytes 和 bytes 转 String 在 Java 8 以及 Java 11 中的性能开销。

  1. /**  

  2.  * @author https://www.wdbyte.com  

  3.  * @date 2021/12/23  

  4.  */  

  5. @State(Scope.Benchmark)  

  6. @Warmup(iterations = 3time = 3 

  7. @Measurement(iterations = 5time = 3 

  8. public class StringInJdk {  

  9.     @Param({"10000"})  

  10.     private int size;  

  11.     private String[] stringArray;  

  12.     private List<byte[]> byteList;  

  13.     @Setup(Level.Trial)  

  14.     public void setup() {  

  15.         byteList = new ArrayList<>(size);  

  16.         stringArray = new String[size]; 

  17.         for (int i = 0; i < size; i++) {  

  18.             String uuid = UUID.randomUUID().toString();  

  19.             stringArray[i] = uuid;  

  20.             byteList.add(uuid.getBytes(StandardCharsets.UTF_8));  

  21.         }  

  22.     }  

  23.     @Benchmark  

  24.     public void byteToString(Blackhole bh) {  

  25.         for (byte[] bytes : byteList) {  

  26.             bh.consume(new String(bytes, StandardCharsets.UTF_8));  

  27.         }  

  28.     }  

  29.     @Benchmark 

  30.  

  31.     public void stringToByte(Blackhole bh) {  

  32.         for (String s : stringArray) {  

  33.             bh.consume(s.getBytes(StandardCharsets.UTF_8));  

  34.         }  

  35.     }  

测试结果:

  1. # JMH version: 1.33  

  2. # VM version: JDK 1.8.0_151, Java HotSpot(TM) 64-Bit Server VM, 25.151-b12  

  3. Benchmark                 (size)   Mode  Cnt     Score     Error  Units  

  4. StringInJdk.byteToString   10000  thrpt   25  2396.713 ± 133.500  ops/s  

  5. StringInJdk.stringToByte   10000  thrpt   25  1745.060 ±  16.945  ops/s  

  6. # JMH version: 1.33  

  7. # VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724  

  8. Benchmark                 (size)   Mode  Cnt     Score     Error  Units  

  9. StringInJdk.byteToString   10000  thrpt   25  5711.954 ±  41.865  ops/s  

  10. 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 倍。关于字符串的操作非常基础,随处可见,可见高版本的优势十分明显。