Java中的5个代码性能提升技巧,最高提升近10倍

文章持续更新,可以关注公众号程序猿阿朗或访问未读代码博客。本文Github.com/niumoo/JavaNotes已经收录,欢迎Star。

这篇文章介绍几个 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 倍性能差异。
当前文章属于Java 性能分析优化系列文章,点击可以查看所有文章。当前文章中的测试使用 JMH 基准测试,相关文章:使用JMH进行Java代码性能测试

预先分配 HashMap 的大小

HashMap 是 Java 中最为常用的集合之一,大多数的操作速度都非常快,但是 HashMap 在调整自身的容量大小时是很慢且难以自动优化,因此我们在定义一个 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)publicclassHashMapSize{@Param({"14"})intkeys;@Param({"16","32"})intsize;@BenchmarkpublicHashMap<Integer,Integer>getHashMap(){HashMap<Integer,Integer>map=newHashMap<>(size);for(inti=0;i<keys;i++){map.put(i,i);}returnmap;}}

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

JMHversion:1.33VMversion:JDK17,OpenJDK64-BitServerVM,17+35-2724Benchmark(keys)(size)ModeCntScoreErrorUnitsHashMapSize.getHashMap1416thrpt254825825.152±323910.557ops/sHashMapSize.getHashMap1432thrpt256556184.664±711657.679ops/s

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

优化 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)publicclassHashMapKey{privateintsize=1024;privateMap<String,Object>stringMap;privateMap<Pair,Object>pairMap;privateString[]prefixes;privateString[]suffixes;@Setup(Level.Trial)publicvoidsetup(){prefixes=newString[size];suffixes=newString[size];stringMap=newHashMap<>();pairMap=newHashMap<>();for(inti=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 callspairMap.put(newMutablePair(prefixes[i],suffixes[i]),i);}}@Benchmark@OperationsPerInvocation(1024)publicvoidstringKey(Blackholebh){for(inti=0;i<prefixes.length;i++){bh.consume(stringMap.get(prefixes[i]+";"+suffixes[i]));}}@Benchmark@OperationsPerInvocation(1024)publicvoidpairMap(Blackholebh){for(inti=0;i<prefixes.length;i++){bh.consume(pairMap.get(newMutablePair(prefixes[i],suffixes[i])));}}}

测试结果:

JMHversion:1.33VMversion:JDK17,OpenJDK64-BitServerVM,17+35-2724BenchmarkModeCntScoreErrorUnitsHashMapKey.pairMapthrpt2589295035.436±6498403.173ops/sHashMapKey.stringKeythrpt259410641.728±389850.653ops/s

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

不使用 Enum.values() 遍历

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

/*** 枚举类遍历测试** @author https://www.wdbyte.com*/@State(Scope.Benchmark)@Warmup(iterations=3,time=3)@Measurement(iterations=5,time=3)@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.MILLISECONDS)publicclassEnumIteration{enumFourteenEnum{a,b,c,d,e,f,g,h,i,j,k,l,m,n;staticfinalFourteenEnum[]VALUES;static{VALUES=values();}}@BenchmarkpublicvoidvaluesEnum(Blackholebh){for(FourteenEnumvalue:FourteenEnum.values()){bh.consume(value.ordinal());}}@BenchmarkpublicvoidenumSetEnum(Blackholebh){for(FourteenEnumvalue:EnumSet.allOf(FourteenEnum.class)){bh.consume(value.ordinal());}}@BenchmarkpublicvoidcacheEnums(Blackholebh){for(FourteenEnumvalue:FourteenEnum.VALUES){bh.consume(value.ordinal());}}}

运行结果

JMHversion:1.33VMversion:JDK17,OpenJDK64-BitServerVM,17+35-2724BenchmarkModeCntScoreErrorUnitsEnumIteration.cacheEnumsthrpt2515623401.567±2274962.772ops/sEnumIteration.enumSetEnumthrpt258597188.662±610632.249ops/sEnumIteration.valuesEnumthrpt2514713941.570±728955.826ops/s

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

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

下面是这次提交的截图:

Java中的5个代码性能提升技巧,最高提升近10倍插图1

使用 Enum 代替 String 常量

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

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

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

/*** @author https://www.wdbyte.com*/@State(Scope.Benchmark)@Warmup(iterations=3,time=3)@Measurement(iterations=5,time=3)publicclassEnumMapBenchmark{enumAnEnum{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 的数量 */privatestaticintsize=10000;/** 随机数种子 */privatestaticintseed=99;@State(Scope.Benchmark)publicstaticclassEnumMapState{privateEnumMap<AnEnum,String>map;privateAnEnum[]values;@Setup(Level.Trial)publicvoidsetup(){map=newEnumMap<>(AnEnum.class);values=newAnEnum[size];AnEnum[]enumValues=AnEnum.values();SplittableRandomrandom=newSplittableRandom(seed);for(inti=0;i<size;i++){intnextInt=random.nextInt(0,Integer.MAX_VALUE);values[i]=enumValues[nextInt%enumValues.length];}for(AnEnumvalue:enumValues){map.put(value,UUID.randomUUID().toString());}}}@State(Scope.Benchmark)publicstaticclassHashMapState{privateHashMap<String,String>map;privateString[]values;@Setup(Level.Trial)publicvoidsetup(){map=newHashMap<>();values=newString[size];AnEnum[]enumValues=AnEnum.values();intpos=0;SplittableRandomrandom=newSplittableRandom(seed);for(inti=0;i<size;i++){intnextInt=random.nextInt(0,Integer.MAX_VALUE);values[i]=enumValues[nextInt%enumValues.length].toString();}for(AnEnumvalue:enumValues){map.put(value.toString(),UUID.randomUUID().toString());}}}@BenchmarkpublicvoidenumMap(EnumMapStatestate,Blackholebh){for(AnEnumvalue:state.values){bh.consume(state.map.get(value));}}@BenchmarkpublicvoidhashMap(HashMapStatestate,Blackholebh){for(Stringvalue:state.values){bh.consume(state.map.get(value));}}}

运行结果:

JMHversion:1.33VMversion:JDK17,OpenJDK64-BitServerVM,17+35-2724BenchmarkModeCntScoreErrorUnitsEnumMapBenchmark.enumMapthrpt2522159.232±1268.800ops/sEnumMapBenchmark.hashMapthrpt2514528.555±1323.610ops/s

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

使用高版本 JDK

String 类应该是 Java 中使用频率最高的类了,但是 Java 8 中的 String 实现相比高版本 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)publicclassStringInJdk{@Param({"10000"})privateintsize;privateString[]stringArray;privateList<byte[]>byteList;@Setup(Level.Trial)publicvoidsetup(){byteList=newArrayList<>(size);stringArray=newString[size];for(inti=0;i<size;i++){Stringuuid=UUID.randomUUID().toString();stringArray[i]=uuid;byteList.add(uuid.getBytes(StandardCharsets.UTF_8));}}@BenchmarkpublicvoidbyteToString(Blackholebh){for(byte[]bytes:byteList){bh.consume(newString(bytes,StandardCharsets.UTF_8));}}@BenchmarkpublicvoidstringToByte(Blackholebh){for(Strings: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-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 转 String 操作上,Java 17 的性能是 Java 8 的 2.5 倍左右,而 String 转 bytes 操作,Java 17 的性能是 Java 8 的 5 倍。关于字符串的操作非常基础,随处可见,可见高版本的优势十分明显。

一如既往,当前文章中的代码示例都存放在github.com/niumoo/JavaNotes.

参考

订阅

可以微信搜一搜程序猿阿朗或访问程序猿阿朗博客阅读。本文Github.com/niumoo/JavaNotes已经收录,有很多知识点和系列文章,欢迎Star。

原创文章 Java中的5个代码性能提升技巧,最高提升近10倍,版权所有
如若转载,请注明出处:https://www.itxiaozhan.cn/20227984.html

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注