Java 性能调优(一)
造成系统瓶颈计算资源的因素有很多,包括CPU资源的调度、内存的读写、磁盘IO、网络IO、数据库、竞争锁、异常等。而系统性能的衡量指标有响应时间、启动时间、执行时间、执行速度、磁盘吞吐量、网络吞吐量、负载承受能力等。那么,从软件的角度看,性能优化的目标有,编写更高效的代码、使用更高效的算法、减少竞争锁、分布式集群、微服务等。而性能的优化策略有,用空间换时间、用时间换空间、优化代码、并行处理等。系统的优化来自三个方面,基础技术、层次方面、架构方面。
Java API调用优化建议
面向对象及基础类型
采用
clone()
方式创建对象:clone()
是Object类中的方法,使用时需实现Cloneable接口。当我们使用new
关键字创建实例的时候,构造函数中所有构造参数都会被自动调用,而clone()
不会调用任何构造函数,避免参数的初始化,在保留原有信息的基础上,创建速度更快。- 拷贝对象返回的是一个新对象,而不是原来对象的引用地址。
- 拷贝对象与
new
关键字操作符返回的新对象的区别是,这个拷贝已经包含了一些原来对象的信息,而不是对象的初始化信息。
避免对boolean的判断:boolean被定义为存储8位(1字节)数值的形式,但只能是true或者false。在使用boolean时,避免使用等于判断,准确的说是不要使用
3=3 == true
这种形式的判断。多用条件操作符:大部分时候,我们在代码中比较多用
if(...) return; else return;
这种形式的判断,但是条件使用太多,对代码的可读性带来影响,因此一些条件语句可以替换为诸如return isdone?0:1
这种形式。静态(static)方法代替实例方法:为了方便的实现多态的支持,实例方法需要维护一张类似虚拟函数导向表的结构,因此,在调用实例方法时会消耗更多的系统资源,使用类加载时就会初始化的静态方法会更快一些。静态方法和实例方法的两点区别:
- 在外部调用静态方法时,可以使用“类名.方法名”或“对象名.方法名”的方式,而实例方法只能通过后者调用,即静态方法无需创建对象。
- 静态方法在访问本类成员时,只允许访问静态成员,而不允许访问实例成员变量和实例方法,,实例方法则无此限制。
有条件的使用final关键字:final关键字可以修饰类、变量、方法,在使用匿名内部类的时候,经常会用到final关键字,如String类。使用final可以锁定方法,以防止任何继承类修改它的含义,在早期的Java版本中,会将final方法转为内嵌调用,JDK6之后无需此优化。
避免不需要的instanceof操作:instanceof关键字是Java的一个二元操作符,和“==”“>”类似,使用instanceof比较左边的对象是否是右边类的实例,会返回true或false。合理利用多态特性,可以避免instanceof的使用。
避免子类中存在父类转换:Java中,所有子类隐含的“等于”父类,因此在子类中无需再转换。
多使用局部变量:调用方法时传递的参数在调用中创建的临时变量都被保存在栈(Stack)里面,因此读写速度更快。像静态变量、实例变量,它们在堆(Heap)中被创建,保留在堆中,读写速度较慢。
- 实例对象属于对象的属性,使用时必须创建对象,其实例对象才会被分配空间,才能使用它。静态变量属于类,只要程序加载了类的字节码,不用创建任何实例对象,就会被分配空间。
使用效率最高的位运算:位运算表达式由操作数和位运算符组成,实现对整数类型的二进制数位运算。位运算符可以分为逻辑运算符(包括
~
、&
、|
、^
)及移位运算符(包括>>
、>>>
、<<
)。>>>
和>>
的区别是,在执行运算时,>>>
运算符的操作数高位补0,而>>
运算符的操作数高位移入原来的高位的值。- 右移以为相当于除以2,左移一位相当于乘以2(位运算速度高于乘除运算)。
- 若进行位逻辑运算的两个操作数的长度不同,则返回值以数据长度较长的数据类型为准。
- 按位异或可以不使用临时变量完成两个值的交换,也可以使某个整型数的特定位的翻转。
- 按位与运算可以用来屏蔽特定的位,也可以用来取某个整型数中特定的位。
- 按位或运算可以用来对某个整型数的特定位置的值置1.
用一维数组代替二维数组:数组的优点是随机访问性能好。二维数组的访问速度要优于一位数组,但是,二维数组占用空间是一维数组的10倍左右。在性能敏感的系统中使用二维数组,如果内存不足,尽量将二维数组替换为一维数组再处理。(二维数组是典型的空间换时间做法)
布尔运算代替位运算:使用位运算代替布尔运算会使系统多很多无效计算,比如
a&&b&&c
,当有一个为false时,会跳过后面的计算直接返回结果,但是位运算需要将所有的表达式都计算完才返回结果。提取表达式优化:将方法中或代码块中的相同的表达式提取出来,避免重复计算,如
a*b*c+d
,没次计算都会消耗资源,提取出来只计算一次即可。不要总是使用取反操作符(!):
!
取反操作符表示异或操作,使用起来方便,但是它会降低程序的可读性。不要重复初始化变量:默认情况下,调用类的构造函数时,Java会把变量初始化为一个确定的值,例如将所有对象设置为
null
,因此,一般情况下,通过构造函数初始化对象。在switch语句中使用字符串:在switch或case表达式中,值不能为
null
,否则会抛空指针异常。在编译时,将字符串hashcode()
之后,转为与整数类型兼容的格式。数字字面常量的改进:可以将十进制、十六进制转为二进制表示,比如数字
9
用二进制表示为0b001001
。当一个数字太长的时候,使用_
分隔符分开,比如5000000
可以改成5_000_000
。优化变长参数方法的调用:一般情况下,方法的参数不宜过多,且最后一个参数为变长参数时,避免和泛型类型一起使用。
基本数据类型-空变量:通过显式的赋空变量,Eden(新生代的一个区域)就能在新对象创建之前获得自由空间,这样垃圾收集就会更快。
集合处理优化方案
- 集合中删除元素:
List list = new ArrayList<~>()
中,不能直接使用remove()
方法删除元素,会发生错误,应该使用Iterator
中的删除方法。 - 集合内部避免返回null:在需要返回数组或集合的方法中,如果需要返回空数据,返回大小为0的数组或集合,避免返回null(在调用方不判空情况下,返回null可能会发生NPE)。
- 使用迭代、流、并行流:JDK8及以上提供,可读性更好,不易出错,容易并行化。比如:
forEach()
是线程安全的。可以对其进行并行处理,stream
替换为parallelStream
(对性能造成严重影响)就可以执行过滤和统计操作。
字符串优化方案
- 善用subString()方法:
String.subString()
可以截取字符串,该方法适用于字符串长度较短时,当字符串长度很大时,会占用很大的内存,造成内存溢出,可以使用new String(smallStr.toCharArray())
替代。因此,当需要截取的字符串长度总和远小于原始字符串文本长度时,使用new String(smallStr.toCharArray())
,在大文本字符串处理中有很大优势。当需要截取的字符串长度总和大于原始文本字符串长度时,使用String.subString()
可以达到共享内存的目的。 - 查找单个字符,用chart()代替startsWith()和endsWith():从性能角度来说,前者会快一些。
- 字符串相加时,仅一个字符,使用’’替代””:使用字符替代。
- 字符串切割:使用
split()
方式切分字符串时性能较差。对比发现,使用StringTokenizer
解析字符串更快。StringTokenizer
类允许一个应用程序进入一个令牌(Token),StringTokenizer
类的对象在内部已经标示化的字符串中维持了当前位置。一些操作使得现有位置上的字符串提前得到处理,一个令牌的值是由获得其曾经创建StringTokenizer
类对象的字符串返回的。 - 字符串连接:字符串连接的方式一般有string对象连接、concat方法、StringBuilder类。而StringBuffer对所有方法都做了同步处理,效率较低,但是在多线程环境下,StringBuilder不是线程安全,无法使用。StringBuilder和StringBuffer两者底层实现都是
char[]
。对比,性能依次为StringBuilder > concat > string+'a'
。 - 正则表达式:正则表达式不是万能的,根据实际情况使用。
其它优化
- 循环优化:应尽可能减少循环。
- 使用arrayCopy():数组复制是一个使用频率很高的功能,
System.arrayCopy()
函数是native函数,通常native函数性能优于普通函数,ArrayList中大量使用该函数。arrayCopy的本质是让处理器利用一条指令处理一个数组中的多个记录,有点像汇编语言里面的串操作指令,只需要指定头指针,然后开始循环即可,即执行一次指令,指针就后移一个位置,操作多少次就循环多少次。 - 使用Buffer进行IO操作:多使用缓冲区。
- 本文链接: https://acehjm.github.io/2018/12/15/Java-性能调优(一)/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!