您的位置 首页 java

编写高性能Java程序的技术汇编

性能并不总是我们需要首要考虑的因素,但当我们需要处理的数据量较大,或者对象、计算需要消耗较多资源时,性能问题就会自然浮上来,需要我们花些心思进行调优。本文搜集整理了40多条编写高性能Java代码的心法,希望可以帮到您。

清理代码并修改算法

不断地重构和整理代码,去除算法中冗余的步骤,使代码更加可读和精炼的同时,通过避免额外的计算来提升性能。

尽量指定类、方法为final

Java编译器会尽量将final的方法内联化,从而大幅提升性能(约50%)。所以,如可能,尽量将对类、方法使用final修饰符。

避免滥用静态变量

如无必要,避免将成员申明为static,否则可能导致该成员持有的对象一直不会被释放,直到程序终止或修改引用。

保持方法简短

过大的方法将消耗更多的内存和CPU周期,而短小精悍的方法将带来更好的内聚性、可读性,和更小的内存占用,从而带来更好的性能。尝试在适当的逻辑点,将过大的方法拆分为合适的小方法。

避免使用递归

递归是一种很好的算法,有切实的好处和实际用途,但是递归本身具有较高的运行成本。如果性能成为问题,可以考虑放弃递归,改用循环。

避免滥用异常

抛出异常需要创建对象、获取堆栈等较重开销,异常只用来处理错误,不要刻意用于控制运行流程。

尽量使用局部变量

调用方法传入的参数、方法创建的临时变量等局部变量都存于栈中,访问时比存于堆中的静态变量、实例变量要快,而且也会随着方法调用结束而被快速清理掉。

减少对变量的重复计算

调用方法存在一定成本,如创建栈帧、保护现场、恢复现场等,所以,如果变量只需计算一次,就可以提前计算好,不要重复计算,减少方法调用。

尽量使用基础类型

将数据存储在栈,而不是堆上,有利于更快地回收内存空间,更快地访问数据。因此,尽量使用基础类型(如int),而不是包装类型(Integer),有助于降低内存消耗,提升性能。

如果可能,尽量使用array,而不是ArrayList。

避免使用BigInteger和BigDecimal

BigInteger和BigDecimal提供了更高的计算精度,但是也会占用更多的内存和CPU,如果允许的话,使用Long和Double作为替代。

使用isEmpty检查String是否为空

String是一个byte数组,isEmpty会判断数组的length,很快就能获得结果。

对于单字符,使用char类型,而不是String

在处理单个字符的字符串时,使用char以获得更好的性能。

使用更轻量级运算

必要时,乘除运算可使用更轻量的位移运算替代。

避免Random实例被多线程使用

多线程共用Random实例时,容易因竞争seed而性能下降,可以使用ThreadLocalRandom作为替代。

使用StringBuilder拼接字符串

使用+号拼接字符串,可能会创建过多的字符串对象。而StringBuffer有同步机制,性能不及StringBuilder,但要注意StringBuilder是线程不安全的。

在单个语句中使用+连接长字符串,以提升可读性,也是可以被编译器优化的。

避免在循环体中使用+拼接字符串,这样可能会导致创建过多的StringBuilder对象。

更好地替换字符串

在Java8及以下版本中,使用Apache Commons StringUtils.replace来替换字符串,比JDK原生的String.replace高效,而更高版本的Java,则使用String.replace会更好。

避免过多if-else语句

在代码中,尤其是循环体中过多地使用if-else语句,将迫使JVM比较这些条件,从而消耗性能。如果业务逻辑中的判断条件过多,可尝试通过分组并计算boolean结果,再在if语句中使用它。

尽可能复用已有对象,而不是随意创建

创建新对象有一定的成本,尤其是大量创建的情况下。如果可以,尽量复用已有对象,甚至使用单例模式,而不是随意创建新的对象,尤其是那些大的对象。

如果存在较多的重复字符串,可以考虑创建这些字符串对象时使用String.intern方法,将字符串放入常量池,再次创建时从常量池获取同一对象,以减少内存占用。

在满足某些条件时,才使用的对象,可以考虑延迟到进入条件区时再创建,而不是提早创建。

避免在循环中声明和创建对象,创建过多的对象引用,可以把声明拿到循环之前。

尽量使用indexOf,而不是split分割字符串

String的split方法中使用了功能更强大的正则表达式,但正则表达式的性能并不总是好,使用不当会导致回溯问题,使CPU居高不下。所以,除非回溯问题可控,尽量使用indexOf来分割字符串。

在任何时候使用正则表达式时,都要尽量采用独占模式,避免使用贪婪模式,避免分支选择,避免捕获组的嵌套等,以避免性能问题。

选择合适的集合类型

ArrayList、HashMap不是线程安全的,而Vector、Hashtable是线程安全的同步集合。当在多线程环境下可能需要线程安全的集合,而不需要线程同步时,尽量使用ArrayList、HashMap,以获得多倍的性能提升。

ArrayList和LinkedList分别基于数组和链表实现,两者的利用迭代器遍历(含foreach)的性能相当,但使用for循环遍历时,ArrayList因为有快速随机访问的特性,性能要高于LinkedList。另外,在ArrayList不发生扩容的情况下,在尾部添加删除元素的性能要略高于LinkedList,而LinkedList在头部添加元素的性能较ArrayList高。

在构造时初始化集合

如果集合内元素可在初始化时确定,则尽量在集合对象构造时初始化进去,而不是先实例化集合对象,再一个一个将元素添加进去。

高效使用HashMap

在已知数据量的情况下,提前设置初始容量(数据量 ÷ 加载因子),避免扩容开销。

一般使用默认的加载因子(0.75)即可,当特别要求充分利用内存资源时,可增大加载因子,而当查询类操作频繁时,可考虑缩减加载因子。

不要在循环中获取集合的大小

如果要遍历集合,那么就提前获取集合的长度,而不是在循环中每次判断集合的大小。

尽量减少集合方法的调用次数

集合提供了很多好用的方法,如size()、containsKey()等,如有可能,尽量减少和避免调用这些方法

使用addAll,而不是add

addAll可以笔add拥有每秒更高的操作数,如果可以,在向ArrayList这种集合添加多个元素时,尽量使用addAll批量添加,而不是通过add方法逐个添加。

使用entrySet,而不是keySet

EntrySet 可以在一秒钟内比 KeySet 多运行 9000 个操作,所以在遍历HashMap时,尽量使用entrySet,而不是keySet。

使用singletonList构造单元素集合

使用singletonList生成单元素集合,比用构造函数new一个更好。

使用EnumSet,而不是HashSet

如果Set中存储的时枚举值(Enum),则最好使用EnumSet,以获得更好的性能。

谨慎使用ArrayList的contains方法

集合大都有一个contains方法,用于判断元素是否已存在于集合中。但是,当ArrayList、Vector这种集合数据量较大时,此方法在最差情况下将遍历整个集合,如果在循环中使用,将带来巨大的性能开销。因此,当需要在大型数据集中搜索时,可考虑使用HashMap替代ArrayList。

必要时使用Stream遍历集合

通常,在数量量少、循环次数较少,以及应用在单核运行的情况下,常规迭代遍历集合的性能较高。如果在多核环境下,对大数据量集合进行迭代,则可以考虑使用由并行机制的Stream。

必要时使用NIO

传统I/O类在高并发、大数据场景下容易发生阻塞,而且数据在内核空间和用户空间的复制也存在性能开销。一般的场景,可通过Buffer解决阻塞问题,而性能要求更高时,就需要引入NIO,通过DirectBuffer、Channel、多路复用等提升性能。

必要时使用高性能通信协议

通常,在需要高性能的分布式环境中,应用服务间调用会采用RPC通信框架,而RMI这种较早的RPC通信方式因序列化性能差、阻塞式I/O、短连接等弊病导致性能不能满足需要。为此,可改为使用Dubbo这种在各方面优化过的通信协议,以满足高并发、小对象传输需要。

必要时使用序列化框架

Java原生的序列化性能较差,如有必要,可使用Protobuf、FastJson、Kryo等序列化框架进行安全、高效地序列化。

谨慎使用字符串作为同步对象

JVM会缓存字符串对象,不同地方声明的String,如果未使用new String,则可能指向同一个字符串对象,如果使用它作为同步对象,则可能产生意想不到的后果。另外,作为一条通用规则,请尽量保持同步块内的处理量为最低限度,以提升性能。

多线程调优技术

对于多线程运行的代码,可考虑以下调优方法:

  1. 在逻辑简单、运算较快的情况下优先考虑单线程模式,而耗时的复杂计算则可以考虑多线程并发
  2. 降低锁的粒度,缩小同步块,分离读写锁,最小化锁的持有时间,以减少锁竞争,提升自旋锁成功率,降低升级为重量级锁的可能性
  3. 仅需要保障可见性、有序性时,可使用开销较小的volatile关键字,避免上下文切换
  4. 设置合理的线程池大小,避免过多线程的上下文切换开销
  5. 适时使用并发容器类,对数据有强一致性要求时,可使用Hashtable、Vector等强一致性容器,而读远大于写时可使用CopyOnWriteArrayList,当数据量较小、查询频繁时,可使用ConcurrentHashMap容器类。

输出前检查日志级别

在输出某个级别的日志之前,先检查该级别是否开启,以避免额外的计算。

避免将大对象输出到日志中

在将对象输出到日志时,仅选取关键的属性作为输出,避免将整个大对象都序列化输出。

缓存昂贵的资源

创建对象有性能开销,有些对象(如数据库连接)的创建过程尤其耗时,而有些对象本身则占用较大资源,将这些对象缓存起来以便再次使用,而不是随意创建,将大大提升性能。

如果可以,尽量使用类似Integer.valueOf方法复用内存中已有对象,而不是用new Integer创建新的对象,尤其是需要大量这种对象的时候。

但是,需要注意,缓存需要额外进行管理,如果合算才使用。

使用预编译的SQL语句

预编译语句(PreparedStatement)具有编译一次而多次使用的特点,比起普通Statement有更好的性能。预编译语句也是安全的,可以避免SQL注入。

只选取需要的数据列,而不是*

SELECT语句中,只返回那些需要的列,可以节省网络带宽,带来更好的性能。

使用正确的表联接

当需要从多个表关联查询数据时,注意使用正确的表联接方式,并在联接字段上建立必要的索引。

使用联接,而不是子查询

相对于联接只遍历一次数据,子查询会多次遍历数据,比联接耗费更多的时间。

使用存储过程,而不是查询

如果查询语句过于复杂而冗长,则考虑改为使用数据库存储过程,以获得存储过程预编译、较少的数据传递等带来的性能。

文章来源:智云一二三科技

文章标题:编写高性能Java程序的技术汇编

文章地址:https://www.zhihuclub.com/186071.shtml

关于作者: 智云科技

热门文章

网站地图