Java中的5个代码性能提升技巧,最高提升近10倍
这篇文章介绍几个Java开发中可以进行性能优化的小技巧,虽然大多数情况下极致优化代码是没有必要的,但是作为一名技术开发者,我们还是想追求代码的更小、更快,更强。如果哪天你发现程序的运行速度不尽人意,可能会想到这篇文章。
提示:我们不应该为了优化而优化,这有时会增加代码的复杂度。
这篇文章中的代码都在以下环境中进行性能测试。JMHversion:1。33(Java基准测试框架)VMversion:JDK17,OpenJDK64BitServerVM,17352724
通过这篇文章的测试,将发现以下几个操作的性能差异。预先分配HashMap的大小,提高14的性能。优化HashMap的key,性能相差9。5倍。不使用Enum。values()遍历,Spring也曾如此优化。使用Enum代替String常量,性能高出1。5倍。使用高版本JDK,基础操作有25倍性能差异。
当前文章属于Java性能分析优化系列文章,点击可以查看所有文章。
当前文章中的测试使用JMH基准测试,相关文章:使用JMH进行Java代码性能测试。预先分配HashMap的大小
HashMap是Java中最为常用的集合之一,大多数的操作速度都非常快,但是HashMap在调整自身的容量大小时是很慢且难以自动优化,因此我们在定义一个HashMap之前,应该尽可能的给出它的容量大小。给出size值时要考虑负载因子,HashMap默认负载因子是0。75,也就是要设置的size值要除于0。75。
相关文章:HashMap源码分析解读
下面使用JMH进行基准测试,测试分别向初始容量为16和32的HashMap中插入14个元素的效率。authorhttps:www。wdbyte。comState(Scope。Benchmark)Warmup(iterations3,time3)Measurement(iterations5,time3)publicclassHashMapSize{Param({14})intkeys;Param({16,32})intsize;BenchmarkpublicHashMapInteger,IntegergetHashMap(){HashMapInteger,IntegermapnewHashMap(size);for(inti0;ikeys;i){map。put(i,i);}returnmap;}}
HashMap的初始容量是16,负责因子0。75,最多插入12个元素,再插入时就要进行扩容,所以插入14个元素过程中需要扩容一次,但是如果HashMap初始化时就给了32容量,那么最多可以承载320。7524个元素,所以插入14个元素时是不需要扩容操作的。JMHversion:1。33VMversion:JDK17,OpenJDK64BitServerVM,17352724Benchmark(keys)(size)ModeCntScoreErrorUnitsHashMapSize。getHashMap1416thrpt254825825。152323910。557opssHashMapSize。getHashMap1432thrpt256556184。664711657。679opss
可以看到在这次测试中,初始容量为32的HashMap比初始容量为16的HashMap每秒可以多操作26次,已经有14的性能差异了。优化HashMap的key
如果HashMap的key值需要用到多个String字符串时,把字符串作为某个类属性,然后使用这个类的实例作为key会比使用字符串拼接效率更高。
下面测试使用两个字符串拼接作为key,和把两个字符串作为MutablePair类的属性引用,然后使用MutablePair对象作为key的运行效率差异。authorhttps:www。wdbyte。comState(Scope。Benchmark)Warmup(iterations3,time3)Measurement(iterations5,time3)publicclassHashMapKey{privateintsize1024;privateMapString,ObjectstringMap;privateMapPair,ObjectpairMap;privateString〔〕prefixes;privateString〔〕suffixes;Setup(Level。Trial)publicvoidsetup(){prefixesnewString〔size〕;suffixesnewString〔size〕;stringMapnewHashMap();pairMapnewHashMap();for(inti0;isize;i){prefixes〔i〕UUID。randomUUID()。toString();suffixes〔i〕UUID。randomUUID()。toString();stringMap。put(prefixes〔i〕;suffixes〔i〕,i);usenewStringtoavoidreferenceequalityspeedinguptheequalscallspairMap。put(newMutablePair(prefixes〔i〕,suffixes〔i〕),i);}}BenchmarkOperationsPerInvocation(1024)publicvoidstringKey(Blackholebh){for(inti0;iprefixes。length;i){bh。consume(stringMap。get(prefixes〔i〕;suffixes〔i〕));}}BenchmarkOperationsPerInvocation(1024)publicvoidpairMap(Blackholebh){for(inti0;iprefixes。length;i){bh。consume(pairMap。get(newMutablePair(prefixes〔i〕,suffixes〔i〕)));}}}
测试结果:JMHversion:1。33VMversion:JDK17,OpenJDK64BitServerVM,17352724BenchmarkModeCntScoreErrorUnitsHashMapKey。pairMapthrpt2589295035。4366498403。173opssHashMapKey。stringKeythrpt259410641。728389850。653opss
可以发现使用对象引用作为key的性能,是使用String拼接作为key的性能的9。5倍。不使用Enum。values()遍历
我们通常会使用Enum。values()进行枚举类遍历,但是这样每次调用都会分配枚举类值数量大小的数组用于操作,这里完全可以缓存起来,以减少每次内存分配的时间和空间消耗。枚举类遍历测试authorhttps:www。wdbyte。comState(Scope。Benchmark)Warmup(iterations3,time3)Measurement(iterations5,time3)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{VALUESvalues();}}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,OpenJDK64BitServerVM,17352724BenchmarkModeCntScoreErrorUnitsEnumIteration。cacheEnumsthrpt2515623401。5672274962。772opssEnumIteration。enumSetEnumthrpt258597188。662610632。249opssEnumIteration。valuesEnumthrpt2514713941。570728955。826opss
很明显使用缓存后的遍历速度是最快的,使用EnumSet遍历效率是最低的,这很好理解,数组的遍历效率是大于哈希表的。
可能你会觉得这里使用values()缓存和直接使用Enum。values()的效率差异很小,其实在某些调用频率很高的场景下是有很大区别的,在Spring框架中,曾使用Enum。values()这种方式在每次响应时遍历HTTP状态码枚举类,这在请求量大时造成了不必要的性能开销,后来进行了values()缓存优化。
下面是这次提交的截图:
使用Enum代替String常量
使用Enum枚举类代替String常量有明显的好处,枚举类强制验证,不会出错,同时使用枚举类的效率也更高。即使作为Map的key值来看,虽然HashMap的速度已经很快了,但是使用EnumMap的速度可以更快。
提示:不要为了优化而优化,这会增加代码的复杂度。
下面测试使用使用Enum作为key,和使用String作为key,在map。get操作下的性能差异。authorhttps:www。wdbyte。comState(Scope。Benchmark)Warmup(iterations3,time3)Measurement(iterations5,time3)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的数量privatestaticintsize10000;随机数种子privatestaticintseed99;State(Scope。Benchmark)publicstaticclassEnumMapState{privateEnumMapmap;privateAnEnum〔〕values;Setup(Level。Trial)publicvoidsetup(){mapnewEnumMap(AnEnum。class);valuesnewAnEnum〔size〕;AnEnum〔〕enumValuesAnEnum。values();SplittableRandomrandomnewSplittableRandom(seed);for(inti0;isize;i){intnextIntrandom。nextInt(0,Integer。MAXVALUE);values〔i〕enumValues〔nextIntenumValues。length〕;}for(AnEnumvalue:enumValues){map。put(value,UUID。randomUUID()。toString());}}}State(Scope。Benchmark)publicstaticclassHashMapState{privateHashMapString,Stringmap;privateString〔〕values;Setup(Level。Trial)publicvoidsetup(){mapnewHashMap();valuesnewString〔size〕;AnEnum〔〕enumValuesAnEnum。values();intpos0;SplittableRandomrandomnewSplittableRandom(seed);for(inti0;isize;i){intnextIntrandom。nextInt(0,Integer。MAXVALUE);values〔i〕enumValues〔nextIntenumValues。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,OpenJDK64BitServerVM,17352724BenchmarkModeCntScoreErrorUnitsEnumMapBenchmark。enumMapthrpt2522159。2321268。800opssEnumMapBenchmark。hashMapthrpt2514528。5551323。610opss
很明显,使用Enum作为key的性能比使用String作为key的性能高出1。5倍。但是仍然要根据实际情况考虑是否使用EnumMap和EnumSet。使用高版本JDK
String类应该是Java中使用频率最高的类了,但是Java8中的String实现相比高版本JDK,则占用空间更多,性能更低。
下面测试String转bytes和bytes转String在Java8以及Java11中的性能开销。authorhttps:www。wdbyte。comdate20211223State(Scope。Benchmark)Warmup(iterations3,time3)Measurement(iterations5,time3)publicclassStringInJdk{Param({10000})privateintsize;privateString〔〕stringArray;privateListbyte〔〕byteList;Setup(Level。Trial)publicvoidsetup(){byteListnewArrayList(size);stringArraynewString〔size〕;for(inti0;isize;i){StringuuidUUID。randomUUID()。toString();stringArray〔i〕uuid;byteList。add(uuid。getBytes(StandardCharsets。UTF8));}}BenchmarkpublicvoidbyteToString(Blackholebh){for(byte〔〕bytes:byteList){bh。consume(newString(bytes,StandardCharsets。UTF8));}}BenchmarkpublicvoidstringToByte(Blackholebh){for(Strings:stringArray){bh。consume(s。getBytes(StandardCharsets。UTF8));}}}
测试结果:JMHversion:1。33VMversion:JDK1。8。0151,JavaHotSpot(TM)64BitServerVM,25。151b12Benchmark(size)ModeCntScoreErrorUnitsStringInJdk。byteToString10000thrpt252396。713133。500opssStringInJdk。stringToByte10000thrpt251745。06016。945opssJMHversion:1。33VMversion:JDK17,OpenJDK64BitServerVM,17352724Benchmark(size)ModeCntScoreErrorUnitsStringInJdk。byteToString10000thrpt255711。95441。865opssStringInJdk。stringToByte10000thrpt258595。895704。004opss
可以看到在bytes转String操作上,Java17的性能是Java8的2。5倍左右,而String转bytes操作,Java17的性能是Java8的5倍。关于字符串的操作非常基础,随处可见,可见高版本的优势十分明显。