Java基础编程调优

前言

JDK 是 Java 语言的基础库,熟悉 JDK 中各个包中的工具类,可以帮助你编写出高性能代码。但是一些基本类不正确使用会有个别的性能问题,需要去注意去调优。

字符串的性能调优

String 对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的。

结果变化:

image-20230605134359364

每次更新就都是为了能够节约内存,提高性能

不可变性:

String 类被 final 关键字修饰了,而且变量 char 数组也被 final 修饰了。而 char[] 被 final+private 修饰,代表了 String 对象不可被更改

这样的好处是为了能够将创建过的字符串缓存在字符串常量池中,以提供给后面的相同字符串引用。

代码优化

  • 多使用StringBuilder和StringBuffer:

    例如:String str= "ab" + "cd" + "ef";

    理论上首先会生成 ab 对象,再生成 abcd 对象,最后生成 abcdef 对象,但是编译器底层做了优化:

    使用StringBuilder来进行拼接,但是会每次创建一个新的StringBuilder,所以在有字符串操作的场景最好自己手动使用StringBuilder和StringBuffer

  • 使用 String.intern() 节省内存:

    在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的信息储存大小降低很多。

  • 字符串的分割:

    Split() 方法使用了正则表达式实现了其强大的分割功能,而正则表达式的性能是非常不稳定的,使用不恰当会引起回溯问题,很可能导致 CPU 居高不下。

    解决方案:

    • 可以用 String.indexOf() 方法代替 Split() 方法完成字符串的分割
    • 如果只能使用正则:
      • 利用在贪婪模式的字符后面加?来开启懒惰模式
      • 利用在贪婪模式的字符后面加+来开启独占模式

ArrayList和LinkedList

ArrayList和LinkedList,一个最最最基本的区别是ArrayList 是基于动态数组实现,LinkedList 是基于双向链表实现。这也是两个集合类各种操作区别的关键点。

ArrayList

ArrayList 实现了 List 接口,继承了 AbstractList 抽象类,底层是数组实现的,并且实现了自增扩容数组大小。并且支持克隆和序列化。还实现了 RandomAccess 接口,表示可以随机访问

  • 关键属性:

    1
    2
    3
    4
    5
    6
    // 默认初始化容量
    private static final int DEFAULT_CAPACITY = 10;
    // 对象数组,transient不是表示不能整个List序列化而是禁止外部自己序列化,ArrayList提供了专门的writeObject以及readObject来实现序列化和反序列化
    transient Object[] elementData;
    // 数组长度
    private int size;
  • 构造函数:

    支持传入初试大小,如果不传入则默认是10;

    我们在初始化 ArrayList 时,可以通过第一个构造函数合理指定数组初始大小,这样有助于减少数组的扩容次数,从而提高系统性能。

  • 新增元素:

    ArrayList 新增元素的方法有两种,一种是直接将元素加到数组的末尾,另外一种是添加元素到任意位置。

    • 加到数组的末尾:速度很快
    • 添加元素到任意位置:会导致在该位置后的所有元素都需要重新排列

    如果容量不够了,会进行1.5倍的大小进行扩容

  • 删除元素:

    在每一次有效的删除元素操作之后,都要进行数组的重组,并且删除的元素位置越靠前,数组重组的开销就越大。

  • 遍历元素:

    由于 ArrayList 是基于数组实现的,所以在获取元素的时候是非常快捷的。

LinkedList

LinkedList 是基于双向链表数据结构实现的,LinkedList 定义了一个 Node 结构,Node 结构中包含了 3 个部分:元素内容 item、前指针 prev 以及后指针 next。

LinkedList 类实现了 List 接口、Deque 接口,同时继承了 AbstractSequentialList 抽象类,LinkedList 既实现了 List 类型又有 Queue 类型的特点;LinkedList 也实现了 Cloneable 和 Serializable 接口,同 ArrayList 一样,可以实现克隆和序列化。

  • 关键属性:

    1
    2
    3
    4
    5
    6
    // 当前大小
    transient int size = 0;
    // 头节点
    transient Node<E> first;
    // 尾节点
    transient Node<E> last;
  • 新增元素:

    LinkedList 添加元素的实现很简洁,但添加的方式却有很多种。

    • 加到链表的末尾头/尾:速度很快
    • 添加元素到任意位置:需要从头/尾节点进行遍历
  • 删除元素:

    首先要通过循环找到要删除的元素,如果要删除的位置处于 List 的前半段,就从前往后找;若其位置处于后半段,就从后往前找。

  • 遍历元素:

    LinkedList 的获取元素操作实现跟 LinkedList 的删除元素操作基本类似,通过分前后半段来循环查找到对应的元素。

    所以在 LinkedList 循环遍历时,我们可以使用 iterator 方式迭代循环,直接拿到我们的元素,而不需要通过循环查找 List。

总结

  • 新增元素:
    • 从集合头部位置新增元素:ArrayList>LinkedList
    • 从集合中间位置新增元素:ArrayList<LinkedList
    • 从集合尾部位置新增元素:ArrayList<LinkedList
  • 删除元素:
    • 从集合头部位置删除元素:ArrayList>LinkedList
    • 从集合中间位置删除元素:ArrayList<LinkedList
    • 从集合尾部位置删除元素:ArrayList<LinkedList
  • 遍历:
    • for(;;) 循环:ArrayList<LinkedList
    • 迭代器迭代循环:ArrayList≈LinkedList

Steam优化

Java8 中添加了一个新的接口类 Stream,他和我们之前接触的字节流概念不太一样,Java8 集合中的 Stream 相当于高级版的 Iterator,他可以通过 Lambda 表达式对集合进行各种非常便利、高效的聚合操作(Aggregate Operation),或者**大批量数据操作 (Bulk Data Operation)**。

例如:过滤分组一所中学里身高在 160cm 以上的男女同学

利用Stream来进行统计:

  • 串行:

    Map<String, List<Student>> stuMap = stuList.stream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex));

  • 并行:(底层利用了ForkJoin 进行分片计算)

    Map<String, List<Student>> stuMap = stuList.parallelStream().filter((Student s) -> s.getHeight() > 160) .collect(Collectors.groupingBy(Student ::getSex));

相关操作

image-20230607150503780

Stream 中的操作分为两大类:

  • 中间操作(Intermediate operations):只对操作进行了记录,即只会返回一个流。
    • 无状态(Stateless):元素的处理不受之前元素的影响。
    • 有状态(Stateful):操作只有拿到所有元素之后才能继续下去。
  • 终结操作(Terminal operations):实现了计算操作。
    • 短路(Short-circuiting):遇到某些符合条件的元素就可以得到最终结果。
    • 非短路(Unshort-circuiting):必须处理完所有元素才能得到最终结果。

使用总结

对常规的迭代、Stream 串行迭代以及 Stream 并行迭代进行性能测试对比

  • 多核 CPU 服务器配置环境下,对比长度 100 的 int 数组的性能;
  • 多核 CPU 服务器配置环境下,对比长度 1.00E+8 的 int 数组的性能;
  • 多核 CPU 服务器配置环境下,对比长度 1.00E+8 对象数组过滤分组的性能;
  • 单核 CPU 服务器配置环境下,对比长度 1.00E+8 对象数组过滤分组的性能。

迭代使用时间

  • 常规的迭代 <Stream 并行迭代 <Stream 串行迭代
  • Stream 并行迭代 < 常规的迭代 <Stream 串行迭代
  • Stream 并行迭代 < 常规的迭代 <Stream 串行迭代
  • 常规的迭代 <Stream 串行迭代 <Stream 并行迭代

其实使用 Stream 未必可以使系统性能更佳,还是要结合应用场景进行选择,也就是合理地使用 Stream。

I/O优化

I/O 是机器获取和交换信息的主要渠道,而流是完成 I/O 操作的主要方式。在NIO出来之前一直使用的是Java 的 I/O 操作类在包 java.io 下的字节流和字符流。

但是传统I/O会有性能瓶颈问题:

  • 多次内存复制:

    数据先从外部设备复制到内核空间,再从内核空间复制到用户空间,这就发生了两次内存复制操作。这种操作会导致不必要的数据拷贝上下文切换,从而降低 I/O 的性能。

  • 阻塞:

    如果没有数据就绪,这个读取操作将会一直被挂起,用户线程将会处于阻塞状态。一旦发生线程阻塞,这些线程将会不断地抢夺 CPU 资源,从而导致大量的 CPU 上下文切换,增加系统的性能开销。

利用NIO进行优化

JDK1.4 发布了 java.nio 包(new I/O 的缩写),NIO 的发布优化了内存复制以及阻塞导致的严重性能问题。JDK1.7 又发布了 NIO2,提出了从操作系统层面实现的异步 I/O。

  1. 使用缓冲区优化读写流操作
  2. 使用 DirectBuffer 减少内存复制
  3. 避免阻塞,优化 I/O 操作

具体使用参考NIO