Java 中的5个代码性能提升技巧

目录
  • 1.预先分配 HashMap 的大小
  • 2.优化 HashMap 的 key
  • 3.不使用 Enum.values() 遍历
  • 4.使用 Enum 代替 String 常量
  • 5.使用高版本 JDK

前言:

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

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

  • 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 倍性能差异。

1.预先分配 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 的性能差异了。

2.优化 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 倍。

3.不使用 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() 缓存优化。

下面是这次提交的截图:

4.使用 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

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

到此这篇关于Java 中的5个代码性能提升技巧的文章就介绍到这了,更多相关Java 代码性能提升技巧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

参考:

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

(0)

相关推荐

  • 分享Java性能调优的11个实用技巧

    大多数开发人员认为性能优化是个比较复杂的问题,需要大量的经验和知识.是的,这并不没有错.诚然,优化应用程序以获得最好的性能并不是一件容易的事情,但这并不意味着你在没有获得这些经验和知识之前就不能做任何事.下面有几个很容易遵循的建议和最佳实践能够帮你创建一个性能良好的应用程序. 这些建议中的大多数都是基于Java的,但是也不一定,也有一些是可以应用于所有的应用程序和编程语言的.在我们分享基于Java的性能调优技巧之前,让我们先讨论一下这些通用的性能调优技巧. 1.在必要之前,先不要优化 这可能是最

  • 通过Java代码技巧改善性能

    前言 程序的性能受到代码质量的直接影响.这次主要介绍一些代码编写的小技巧和惯例.虽然看起来有些是微不足道的编程技巧,却可能为系统性能带来成倍的提升,因此还是值得关注的. 慎用异常 在Java开发中,经常使用try-catch进行错误捕获,但是try-catch语句对系统性能而言是非常糟糕的.虽然一次try-catch中,无法察觉到她对性能带来的损失,但是一旦try-catch语句被应用于循环或是遍历体内,就会给系统性能带来极大的伤害. 以下是一段将try-catch应用于循环体内的示例代码: @

  • Java 中的5个代码性能提升技巧

    目录 1.预先分配 HashMap 的大小 2.优化 HashMap 的 key 3.不使用 Enum.values() 遍历 4.使用 Enum 代替 String 常量 5.使用高版本 JDK 前言: 提示:我们不应该为了优化而优化,这有时会增加代码的复杂度. 这篇文章中的代码都在以下环境中进行性能测试. JMH version: 1.33(Java 基准测试框架) VM version: JDK 17, OpenJDK 64-Bit Server VM, 17+35-2724 通过这篇文章

  • Java性能优化技巧汇总

    本文实例汇总了Java性能优化技巧.分享给大家供大家参考.具体分析如下: 这里参考了些书籍,网络资源整理出来,适合于大多数Java应用 在JAVA程序中,性能问题的大部分原因并不在于JAVA语言,而是程序本身.养成良好的编码习惯非常重要,能够显著地提升程序性能. 1.尽量使用final修饰符. 带有final修饰符的类是不可派生的.在JAVA核心API中,有许多应用final的例子,例如java.lang.String.为String类指定final防止了使用者覆盖length()方法.另外,如

  • 十分简单易懂的Java应用程序性能调优技巧分享

    大多数开发人员理所当然地以为性能优化很复杂,需要大量的经验和知识.好吧,不能说这是完全错误的.优化应用程序以获得最佳性能不是一件容易的事情.但是,这并不意味着如果你不具备这些知识,就不能做任何事情.这里有11个易于遵循的建议和最佳实践可以帮助你创建一个性能良好的应用程序. 大部分建议是针对Java的.但也有若干建议是与语言无关的,可以应用于所有应用程序和编程语言.在讨论专门针对Java的性能调优技巧之前,让我们先来看看通用技巧. 1.在你知道必要之前不要优化 这可能是最重要的性能调整技巧之一.你

  • Java程序员编程性能优化必备的34个小技巧(总结)

    1.尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 控制资源的使用,通过线程同步来控制资源的并发访问: 控制实例的产生,以达到节约资源的目的: 控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信. 2.尽量避免随意使用静态变量 要知道,当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如: 此时静态变量b的生命周期与A类同步,如

  • java中synchronized(同步代码块和同步方法)详解及区别

     java中synchronized(同步代码块和同步方法)详解及区别 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问题,当有多条线程同时访问共享数据时,如果进行同步,就会发生错误,Java提供的解决方案是:只要将操作共享数据的语句在某一时段让一个线程执行完,在执行过程中,其他

  • java 中同步方法和同步代码块的区别详解

    java 中同步方法和同步代码块的区别详解 在Java语言中,每一个对象有一把锁.线程可以使用synchronized关键字来获取对象上的锁.synchronized关键字可应用在方法级别(粗粒度锁)或者是代码块级别(细粒度锁). 问题的由来: 看到这样一个面试题: //下列两个方法有什么区别 public synchronized void method1(){} public void method2(){ synchronized (obj){} } synchronized用于解决同步问

  • Java中EnumSet代替位域代码详解

    本文研究的主要是Java中EnumSet代替位域的相关内容,具体介绍如下. 读书笔记<Effective Java 中文版 第2版> 位域表示法允许利用位操作,有效地执行先 union(联合)和 intersection(交集)这样的集合操作.但是位域有着int枚举常亮的所有缺点,甚至更多.当位域一数字形式打印时,翻译位域比翻译简单的int枚举常量要困难得多.甚至,要遍历位域表示的所有元素都没有很容易的方法. //Bit field enumeration constant - OBSOLET

  • Java中使用HashMap改进查找性能的步骤

    Java中,HashMap,其实就是键值对.一个Key,对应一个值:写数据时,指定Key写对应值:读取时凭Key找到相应值.感觉就跟Redis差不多. // 创建 HashMap 对象 Sites HashMap<Integer, String> Sites = new HashMap<Integer, String>(); // 添加键值对 Sites.put(1, "Google"); Sites.put(2, "Runoob"); Si

  • Java中16条的代码规范

    目录 一.MyBatis 不要为了多个查询条件而写 1 = 1 二. 迭代entrySet() 获取Map 的key 和value 三.使用Collection.isEmpty() 检测空 四.初始化集合时尽量指定其大小 五.使用StringBuilder 拼接字符串 六.若需频繁调用Collection.contains 方法则使用Set 七.使用静态代码块实现赋值静态成员变量 八.删除未使用的局部变量.方法参数.私有方法.字段和多余的括号. 九.工具类中屏蔽构造函数 十.删除多余的异常捕获并

  • Java中Math类常用方法代码详解

    近期用到四舍五入想到以前整理了一点,就顺便重新整理好经常见到的一些四舍五入,后续遇到常用也会直接在这篇文章更新... public class Demo{ public static void main(String args[]){ /** *Math.sqrt()//计算平方根 *Math.cbrt()//计算立方根 *Math.pow(a, b)//计算a的b次方 *Math.max( , );//计算最大值 *Math.min( , );//计算最小值 */ System.out.pri

  • 详解java中的四种代码块

    在java中用{}括起来的称为代码块,代码块可分为以下四种: 一.简介 1.普通代码块: 类中方法的方法体 2.构造代码块: 构造块会在创建对象时被调用,每次创建时都会被调用,优先于类构造函数执行. 3.静态代码块: 用static{}包裹起来的代码片段,只会执行一次.静态代码块优先于构造块执行. 4.同步代码块: 使用synchronized(){}包裹起来的代码块,在多线程环境下,对共享数据的读写操作是需要互斥进行的,否则会导致数据的不一致性.同步代码块需要写在方法中. 二.静态代码块和构造

  • java中的arrays.sort()代码详解

    Arrays.sort(T[], Comparator < ? super T > c) 方法用于对象数组按用户自定义规则排序. 官方Java文档只是简要描述此方法的作用,并未进行详细的介绍,本文将深入解析此方法. 1. 简单示例 sort方法的使用非常的简单明了,下面的例子中,先定义一个比较Dog大小的Comparator,然后将其实例对象作为参数传给sort方法,通过此示例,你应该能够快速掌握Arrays.sort()的使用方法. import java.util.Arrays; impo

  • java中switch选择语句代码详解

    switch结构(开关语句)的语法 switch(表达式 ){ --->类型为int.char case 常量1 :--->case 结构可以有多个 //语句块1 break; --->程序跳出switch结构 case 常量n :--->常量的值不能相同 //语句块n break; default:--->和if结构中的 else作用相同 //语句块 break; } 下面看一段代码示例,有详细的注释,大家可以参考: public class SwitchStu{ /* s

随机推荐