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
通过这篇文章的测试,将发现以下几个操作的性能差异。
- 预先分配 HashMap 的大小,提高 1/4 的性能。
- 优化 HashMap 的 key,性能相差 9.5 倍。
- 不使用 Enum.values() 遍历,Spring 也曾如此优化。
- 使用 Enum 代替 String 常量,性能高出 1.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)
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% 次,已经有 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)
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
可以发现使用对象引用作为 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)
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 遍历效率是最低的,这很好理解,数组的遍历效率是大于哈希表的。
可能你会觉得这里使用 values() 缓存和直接使用 Enum.values() 的效率差异很小,其实在某些调用频率很高的场景下是有很大区别的,在 Spring 框架中,曾使用 Enum.values() 这种方式在每次响应时遍历 HTTP 状态码枚举类,这在请求量大时造成了不必要的性能开销,后来进行了 values() 缓存优化。
下面是这次提交的截图:

使用 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)
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 的数量 */
private static int size = 10000;
/** 随机数种子 */
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-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 倍。但是仍然要根据实际情况考虑是否使用 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)
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-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.
参考
- 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
订阅
本文 Github.com/niumoo/JavaNotes 已经收录,有很多知识点和系列文章,欢迎Star。

Java 中的5个代码性能提升技巧,最高提升近10倍的更多相关文章
- Java中常用的设计模式代码与理解
Java中常用的设计模式代码与理解 一.单例模式 1.饿汉式 (太饿了,类加载的时候就创建实例) /** * 饿汉式单例模式 */ public class HungrySingleInstance ...
- 在Java中直接调用js代码(转载)
http://blog.csdn.net/xzyxuanyuan/article/details/8062887 JDK1.6版添加了新的ScriptEngine类,允许用户直接执行js代码. 在Ja ...
- 在Java中直接调用js代码
JDK1.6版添加了新的ScriptEngine类,允许用户直接执行js代码. 在Java中直接调用js代码 不能调用浏览器中定义的js函数,会抛出异常提示ReferenceError: “alert ...
- Python 代码性能优化技巧(转)
原文:Python 代码性能优化技巧 Python 代码优化常见技巧 代码优化能够让程序运行更快,它是在不改变程序运行结果的情况下使得程序的运行效率更高,根据 80/20 原则,实现程序的重构.优化. ...
- C#中使用泛型对照使用通用基础类型效率减少近一倍
C#中使用泛型对照使用通用基础类型效率减少近一倍 以下是測试结果: CSharp class and generic TotalMilliseconds: 270772.9229CSharp g ...
- java中的synchronized同步代码块和同步方法的区别
下面这两段代码有什么区别? //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized ...
- 彻底理解Java中的基本数据类型转换(自动、强制、提升)
说基本数据类型转换之前,先了解下 Java 中的 8 种基本数据类型,以及它们的占内存的容量大小和表示的范围,如下图所示. 重新温故了下原始数据类型,现在来解释下它们之间的转换关系. 自动类型转换 自 ...
- 浅谈java中异常抛出后代码是否会继续执行
问题 今天遇到一个问题,在下面的代码中,当抛出运行时异常后,后面的代码还会执行吗,是否需要在异常后面加上return语句呢? public void add(int index, E element) ...
- Python代码性能优化技巧
摘要:代码优化能够让程序运行更快,可以提高程序的执行效率等,对于一名软件开发人员来说,如何优化代码,从哪里入手进行优化?这些都是他们十分关心的问题.本文着重讲了如何优化Python代码,看完一定会让你 ...
随机推荐
- Linux 使用wpa_supplicant手动配置连接wifi
Linux 使用wpa_supplicant手动配置连接wifi wpa_supplicant 简介 wpa_supplicant是Linux BSD, Mac OSX和Windows的WPA的服务, ...
- 2017Java前景怎么样?
当今社会互联网软件行业属于高薪技术行业,伴随着互联网的发展Java在Web领域的优势也日渐凸显,并且java语言本身就应用最广泛,最高效.据说,全球有25亿Java器件运行着Java,450多万Jav ...
- CF1354F Summoning Minions
考虑我们一定是先放我们选定了\(m\)个数,一定是先放了\(m-1\)个数上去,然后让放上一个不打算选的然后拿下来,白嫖\(b * (m-1)\)的贡献,最后放上一个打算放的. 考虑我们一定是按\(b ...
- PHP-FPM运行状态的实时查看及监控详解
https://www.jb51.net/article/97640.htm https://blog.csdn.net/Dr_cokiy/article/details/105580758
- lua_newthread的真正意义
lua_newthread 这个接口,存在误导性,很多人第一次试图用它来解决多线程问题时,都会入坑. 实际上,这个接口真正的用法,是给那些在lua更底层的某些行为(通常是递归)导致了lua的栈溢出而准 ...
- euerka总结
一.euerka的基本知识 1. 服务治理 Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理 在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系 ...
- 7个连环问揭开java多线程背后的弯弯绕
摘要:很多java入门新人一想到java多线程, 就会觉得很晕很绕,什么可见不可见的,也不了解为什么sync怎么就锁住了代码. 本文分享自华为云社区<java多线程背后的弯弯绕绕到底是什么? 7 ...
- Learning Spark中文版--第四章--使用键值对(2)
Actions Available on Pair RDDs (键值对RDD可用的action) 和transformation(转换)一样,键值对RDD也可以使用基础RDD上的action(开工 ...
- GO 定时器NewTimer、NewTicker使用
package main import ( "fmt" "sync" "time" ) /** *ticker只要定义完成,从此刻开始计时, ...
- Can we access global variable if there is a local variable with same name?
In C, we cannot access a global variable if we have a local variable with same name, but it is possi ...