搬砖者:为什么程序总是那么慢?它现在到底在干什么?时间都花到哪里去了?
面试官:简单谈谈 Java 程序性能优化?
1.
研发过程中,String 的 API 用的应该是最多,创建 String 对象,以及字符串分割处理那是常有的事儿。
1.1. 字符串分割,谁更胜一筹?
字符串分割,常用的方式有哪些?哪种方式好一些?
方式一,经常用 String 提供的 split() 方法来满足业务需求。
代码模拟了一些数据,然后程序跑起来,花费大约 3000 多毫秒。
方式二,采用字符串分割的工具类 StringTokenizer。
采用 StringTokenizer 完成 split() 同样的数据分割,花费大约 500 毫秒。
从运行效果, StringTokenizer 其效率高于 split() 方法。 所以,在能够使用 StringTokenizer 进行处理的地方,就尽量使用 StringTokenizer 进行字符串分割处理。
另外,平时研发中,需要注意一点,在使用索引访问用 String 的 split() 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛异常的风险。
1.2. 字符串拼接,哪种方式更优?
方式一,使用 + 号拼接字符串。
程序跑起来,大约花费 27687 毫秒。
方式二,使用 StringBuilder 进行拼接字符串。
程序跑起来,大约花费 24 毫秒。
另外,在阿里开发手册中也强烈推荐, 在循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。如果采取 + 号拼接,会造成内存资源浪费。
2.
数组复制是研发过程中,使用较多的功能,JDK 中提供了 API 来实现。但是,哪种方式较好呢?
方式一,作为开发人员,没事就喜欢造轮子,代码会这么写。
方式二,采取 System.arraycopy() 来完成数组复制。
红色圈住部分,在本机跑起来验证,方式一、方式二,差距不是特别大,在数据量大的时候,System.arraycopy() 还是稍微好一点。
方式三,采取 Arrays.copyOf() 来完成数组的复制。
鉴于 Arrays.copyOf() 底层还是调用 System.arraycopy() 来实现,性能肯定稍逊色于直接调用 System.arraycopy() 来完成数组复制。
3.
尽可能让程序少做重复的计算,尤其要重点关注循环体内的代码。
举个简单的栗子,上面代码段中,Math.PI * Math.sin(k) 的执行在循环体中重复执行,而且执行结果是唯一的,可以考虑提到循环体外。
本机进行验证时,前者大约花费 30 毫秒,而调整后,大约花费 2 毫秒,性能提升还是有的。
另外,在阿里开发手册中强烈推荐, 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。
4.
业务研发中,集合家族的 API 使用频率相当之高。那么, 充分的选择好数据结构进行数据存储,便是最好的程序优化 。
为了更清晰的说清各自的使用场景,也为了更好的助你掌握,梳理成思维导图。
4.1. List 家族,谁能得宠?
4.2 . Map 家族,谁占鳌头?
另外, 在集合初始化时,要指定集合初始值大小。
说明: HashMap 使用 HashMap(int initialCapacity) 初始化。
正例: initialCapacity = (需要存储的元素个数 / 负载因子) + 1。
注意负载因子(即 loader factor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例: HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素不断增加,容 量 7 次被迫扩大,resize 需要重建 hash 表,严重影响性能。
—— 阿里开发手册
4.2. Set 家族,谁最适配?
图中已经把常用的数据结构列举了出来,就不再一一去写代码验证啦。 还是那句话, 选择好数据结构进行数据存储,便是最好的程序优化。
5.
缓冲,最常用的场景就是提高 I/O 的速度,解决 I/O 性能瓶颈。在
Java 中对不少 I/O 组件都提供了缓冲功能。
例如,采用 FileWriter 向文件中写入数据。
程序跑起来,花费大约 8212 毫秒。那么,再来看看加入缓冲之后会有什么效果?
程序跑起来,花费大约 4143 毫秒,性能提升了一倍。
6.
在实际项目研发中,缓存也是经常使用到,缓存是为了提升系统性能而开辟的内存空间。
最为简单的缓存可以直接使用 HashMap 实现,例如应用的配置信息,在启动的时候都加载进去。
针对银行编码等一些使用频率较高的 业务 数据,或者来之不易的计算结果,都可以保存到缓存中,当再次使用时,直接从缓存中获取,而不需要再占用宝贵的系统资源。
目前有很多基于 Java 的缓存框架,而我用的最多的是 EhCache。
7.
推荐: 谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使 用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明: 大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。
记录日志时请思考: 这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
—— 阿里开发手册
8.
一个优化原则。 先实现业务功能,再考虑优化性能,如果功能都没实现,谈其它的都白扯。
首先从系统设计层面,去看看是否有改进的可能,是不是可以引入一些设计模式、是不是可以引入缓存机制等方法,来屏蔽潜在的性能问题。
然后从代码层面,看看代码是否有优化的可能。
接着去看看 Java 程序运行的环境,也就是通过调整 JVM 的参数来提升一下性能。
接着到数据库层面,看看是否有调优的可能。
最后到操作系统层面,看看是否可以进行调优。
好了,本次的分享,就到这里,希望你们喜欢。
推荐阅读:
程序员写出这样的代码,能不挨骂吗?
拥有这些工具,还怕干不出好活?