您的位置:首页 > 编程语言 > Java开发

java性能优化笔记(三)java程序优化

2016-09-13 06:53 267 查看
程序代码优化要点:

字符串优化:分析String源码,了解String常用方法,使用StringBuffer、StringBuilder。

List、Map、Set优化:分析常用ArrayList、LinkedList、HashMap、TreeMap、LinkedHashMap、Set接口、集合常用方法优化。

使用NIO:Buffered、Channel操作和原理,使用零拷贝。

引用优化:强引用、弱引用、软引用、虚引用、WeekHashMap。

优化技巧:常用代码优化技巧。这里不一一罗列,请参考下面的详解。

字符串优化:

String对象特点:

终态:String类被声明为final,不可被继承重写,保护了String类和对象的安全。在jdk1.5之前final声明会被inline编译,性能大幅度提高,jdk1.5之后性能提升不大。

常量池:String在编译期间会直接分配在方法区的常量池中,当我们写了多个相同值的String对象时,它们实际是指向了同一空间的不同引用罢了。这样对于String这样经常使用的对象访问代价和创建代价是十分低的。需要注意的是当使用
String a="123";String b=new String("123");
的时候,编译器虽然会创建一个新的String实例,但是实际值依然是指向常量池中的已有的123。我们可以使用a.intern(),String的intern方法返回常量池中的引用,intern是一个native本地方法。

不变性:String对象生成后内存空间永久不会变化,好处是在多线程的情况下不用加锁同步操作。需要注意如下代码:
String a="123";a="456";
只是改变了对象的引用所指向的位置,实际的”123”是不变的。

关于内存泄漏:

存在内存泄漏的方法:

String:

substring(int,int):



可以看到substring方法中使用了
return ((beginIndex == 0) && (endIndex == value.length)) ? this : new String(value, beginIndex, subLen)
构建截取的新字符串,来看看new String三参的构造函数,



最后String使用了数组拷贝
this.value = Arrays.copyOfRange(value, offset, offset+count);
这样做的好处是以空间换取了时间,快速的实现了新字符串的产生。但是当我们构造一个大的字符串进行截取时,并且进行批量截取时,可以想到字节拷贝将会耗费很大内存,存在内存泄漏的问题。这是因为substring使用的三参构造函数返回的字符串被外界调用者保持着强引用,而内存拷贝量大,gc无法回收,所以会产生OutOfMemory异常。

new String(char[],int,int):根据上述分析,这个三参的构造函数是罪魁祸首,所以建议不要使用。注意在jdk1.7之前该构造函数是只可以在包内使用的,但是1.7以后变成了公有方法。

substring(int):与两参的substring一样,单参的也是调用了new String(char[],int,int),参考如下代码:



注意jdk1.7之前该函数调用的是两参的substring。

concat(String):与substring一样,在jdk1.7之前使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,jdk1.7以后算法改进不会出现该问题。

replace(char,char):与substring一样,在jdk1.7之前使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,jdk1.7以后算法改进不会出现该问题。

valueOf(char[],int,int):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。

copyValueOf(char[],int,int):同上。。。

toLowerCase(Locale):同上。。。

toUpperCase(Locale):同上。。。

Integer:

toString(int):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。

Long:

toString(long):与substring一样,但在jdk1.7之前和之后使用的是new String(char[],int,int)返回的新字符串会出现内存泄漏问题,并没有什么改进。

修复内存泄漏:解决这个内存泄漏的方法是可以
new String(str.substring(0,100));
,构造一个新字符串接触了substring原来的强引用,让gc可以正常回收,就不会出现OutOfMemory异常了。至于其他都调用了三参构造函数的,也可以使用new String对返回值重新创建实例解除强引用,也可以自己实现这些函数的功能避免调用String的三参构造函数。

关于字符串分割和查找:

String的split:

split实现中使用了正则表达式,在大量字符串分割时正则表达式会贪婪匹配,效率会降低,不推荐使用。

StringTokenizer的使用:

StringTokenizer是jdk自带的字符串分割工具,由于没有使用正则匹配,所以速度更快,可以参看如下源码:





StringTokenizer只是使用字符串本身的属性进行了切分。

StringBuffer和StringBuilder:

区别:StringBuffer是线程安全的,所有操作字符串的方法都做了synchronized操作,而StringBuilder没有,是线程不安全的,所以StringBuffer性能低于StringBuilder。

注意事项:StringBuffer和StringBuilder都提供了带有capacity参数的构造函数,主要作用是指定初始化容量(保存字符串缓冲区)的大小,当容量超过capacity时,会进行扩容,扩容为原来大小的2倍,创建新内存空间,同时把原来空间的内存拷贝到新内存空间,然后释放原内存空间。由于内存拷贝很耗时,所以最好指定适当的capacity。

与String的+号对比:当使用+号拼接字符串时,编译器会把+号替换成
new StringBuilder().append()
,提高拼接效率,但是在大量循环拼接时,编译器不够智能,每次都生成新的StringBuilder,产生大量gc,所以性能不高,最好在循环中使用conact或自己构建StringBuffer或StringBuilder。

List接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-List接口分析》//TODO

Map接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-Mapt接口分析》//TODO

Set接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-Set接口分析》//TODO

RadnomAccess接口: 由于篇幅过长,故拆分,请参考《java性能优化笔记-RadnomAccess接口分析》//TODO

优化集合操作:

分离循环中重复代码:最常见的是循环中调用集合的size()方法,如果集合容量不变,一定要把size提前求出来,
int size=list.size();for(int i=0;i<size;i++){...}
,虽然size()方法返回的是集合的内部变量size,但是由于size()是方法,存在函数的入栈出栈,会耗时。

减少方法调用:与上面一样,方法调用存在函数入栈出栈,所以最好不要在遍历集合中调用方法。

省略相同操作:在遍历集合时我们经常会通过get方法获取集合中的对象再对其操作,如果在遍历中多次使用get也是耗时的,所以可以在循环体内先get出对象存到局部变量中,然后操作局部变量。类似这样的重复操作可以提取出来。

使用迭代器:遍历集合的方法有很多,通过for随机访问,foreach迭代,迭代器迭代等。

for随机访问:在对ArrayList时迭代相当快,LinkedList基于链表实现随机访问非常差。

迭代器迭代:迭代器访问集合的速度是最快的,每个集合都实现了Ietrator迭代器接口,每个实现都会根据集合本身特性优化访问数据。

foreach迭代:由于foreach会被编译成迭代器,正常理应访问速度快,但是编译后会存在一次对迭代器next()返回变量的多余赋值,所以速度有所减缓。

使用NIO:

NIO与传统I/O区别:NIO与传统I/O最大的区别在于NIO使用了异步技术,通过了缓冲区操作API和管道操作,同时支持直接内存分配操作,提供了epoll和select的支持,支持内存映射操作。而传统I/O是同步阻塞的,读写操作会阻塞当前线程,等待完成。传统I/O提供的API中虽然也有Buffered,但没NIO实现的健全。而使用Netty,基于NIO扩展的开发框架,提供了更强大的异步通信功能。

Buffer和Channel:

Buffer:NIO提供的Buffer比较全面,不但对数据类型分别做了封装,而且还提供零拷贝的操作,加快了内存访问速度。Buffer包含:CharBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、FloatBuffer、ByteBuffer以及MappedByteBuffer。

Channel: NIO创建了双向读写通道,通过通道发送Buffer,一端发送数据另一端无需等待,收到数据后进行处理,从而实现异步。Channel包含:FileChannel、DatagramChannel、SocketChannel、ServerSocketChannel。

Buffer原理:Buffer缓冲区中有4个重要的属性:

position:

读:当将Buffer从写模式切换到读模式,position会被重置为0。 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。

写:position表示当前的位置。初始的position值为0,当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。

limit:

读:当切换Buffer到读模式时, limit表示最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)

写:表示最多能往Buffer里写多少数据。 写模式下,limit等于capacity。

capcity:Buffer的最大容量,一旦满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。

mark :用于对Buffer进行打标志位,通过mark()方法对当前位置打标签,可以在后续通过rest()回到这个标志位重新操作。

API:

Buffer的常用的接口:

allocate()及allocateDirect():

allocate():Buffer的静态方法,每一个Buffer的子类都有该方法,用于在堆中分配内存。

allocateDirect():Buffer的静态方法,每一个Buffer的子类都有该方法,用于在操作系统的直接内存总分配空间,不在堆中,所以访问速度快,不受java堆限制,不受gc控制。可通过-XX:MaxDirectMemorySize设置allocateDirect的最大空间。

rewind():将position设回0,可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素。mark被清空,capacity不变。

clear():将position设回0,limit被设置成capacity。mark被清空,capacity不变。

flip():将position设回0,limit设置为position,用于读写的切换。mark被清空,capacity不变。

remaining():返回limit-position,既有效的读取位数。

hasRemaining():若有有效数据可读,返回true。

position() 和重载:无参position()返回position的位置,position(int)设置position的位置。

limit()和重载:无参limit()返回limit的位置,limit(int)设置limit的位置。

wrap():将数组包装为对应的Buffer。

mark():标记当前position位置,在后续可用reset()回到标记位。

reset():讲position设置为mark的位置,用于重新读取。

duplicate():从一个Buffer中再复制出完全一样的一份,生成的新缓冲区共享内存数据,并且对任意一份改动都是相互可见的,但两份都独立维护自己的position、limit和mark。可用于多线程中无锁的实现。

slice():将现有的Buffer切分出来,创建子缓冲区,两份缓冲区共享内存数据,并且对任意一份改动都是相互可见的。通过设置position和limit的位置,然后调用该方法进行切片。可用于系统的模块化,在子模块中处理子缓冲区。

asReadOnlyBuffer():得到当前Buffer的只读副本,同样是共享内存数据。在原始缓冲区改的数据在只读缓冲区依然可见。保证了安全性。

get()及其重载:

get():返回当前到position位置的数据,并将position位置向后移动一位。

get(byte []):读取当前Buffer数据到给定的字节数组中,并且移动position到读去到字节数组范围的下一位。

get(int):直接读取给定索引上的数据,不移动position。

put()及其重载:

put(byte):在position当前位置写入字节,position向后移动一位。

put(int,byte):在给定索引位写入数据,position向后移动一位。

put(byte[]):在position当前位置写入字节数组,position向后移动一位。

Channel的常用接口:

获取Channel:jdk在基本的InputStream和OutputStream上都封装了获取Channel的方法getChannel(),该方法直接返回Channel对象。

read():读取数据到给定的Buffer中。

write():写入数据到给定的Buffer中。

散射和聚集:

散射(Scattering):从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“散射(scatter)”到多个Buffer中。NIO提供了ScatteringByteChannel。

聚集(Gathering):写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。NIO提供了GatheringByteChannel。

每个FileChannel、DatagramChannel、SocketChannel都实现了ScatteringByteChannel和GatheringByteChannel。都可以通过read()和write()方法构建多个Buffer数组的大缓冲区块。

散射和聚集最大的用处在于系统的模块化或者传输协议的模块化,将不同模块散射或分散到不同Buffer数组中,方便一块一块的结构化读取和写入。但需要注意的是Scattering的read()方法按照buffer在数组中的顺序将从channel中读取的数据写入到buffer,当一个buffer被写满后,channel紧接着向另一个buffer中写。Scattering Read在移动下一个buffer前,必须填满当前的buffer。而Gathering的write()方法与其相反,写入只是对position和limit的有效数据操作,所以不填满一个Buffer数组一样可以进行下一个数组的填充。

使用MappedByteBuffer:MappedByteBuffer用于将文件数据映射到内存空间中,支持读写和只读的内存映射(通过FileChannle的map方法的第一个参数指定,值:FileChannle.MapMode.READ_WRITE和FileChannle.MapMode.READ_ONLY)。由于直接将文件数据映射到内存中,所以访问文件速度较快,但是注意大文件映射会超出内存空间大小限制而无法映射。MappedByteBuffer要比ByteBuffer和BufferedInputStream或BufferedOutputStream速度快几个数量级。

更多NIO技术探讨我会单独发一系列文章。

引用类型:

强引用:直接new出的对象都是强引用的,强引用gc回收很少,除非与gc root彻底断开,否则gc宁可抛出OutOfMemory异常。

软引用:软引用会在堆接近阈值的时候被gc回收,只要有足够的内存就会保持引用。使用java.lang.SoftReference构造软引用。

弱引用:软引用的引用级别最低,只要gc线程运行时发现软引用的存在就会回收弱引用,不过gc线程优先级很低,所以也会存活一段时间。使用java.lang.WeekReference构造弱引用。

虚引用:虚引用是无法直接引用的,当使用java.lang.PhantomReference构造虚引用后用get()方法取出原来的强引用时,会直接得到null,因为虚引用get()方法实现直接返回的null。虚引用的唯一作用是配合引用队列回收资源,在gc回收强引用时进入引用队列,在引用队列中通过引用队列的remove()或poll()方法的返回值判断是否被回收,如果回收的话清理其他资源。

WeekHashMap:WeakHashMap是HashMap的弱引用版本,里面每个Key的元素都是弱引用的。WeakHashMap继承WeekReference用于把Key放入弱引用中,在get或者put时也会直接或间接调用内部方法expungeStaleEntries(),该方法会检测弱引用是否被回收,如果被回收会释放Key的资源。

引用队列:当对象改变其可达性状态时,对该对象的引用就可能会被置于引用队列(reference queue)中。这些队列被垃圾回收器用来与我们的代码沟通有关对象可达性变化的情况。java.lang.ReferenceQueue,在软引用、弱引用、虚引用构造函数中传入,当gc线程回收时,会把对象放入引用队列,但是它们不会被清除。一旦引用对象被垃圾回收器插人到队列中,其get方法的返回值就肯定会是null,因此该对象就再也不能复活了。

public Reference < ? extends下>poll ():用于移除并返回该队列中的下一个引用对象,如果队列为空,则返回null.

public Referenceremove ()throws InterruptedException:用于移除并返回该队列中的下一个引用对象,该方法会在队列返回可用引用对象之前一直阻塞。

public Referenceremove (long timeout) throws interrupte-dException:用于移除并返回队列中的下一个引用对象。该方法会在队列返回可用引用对象之前一直阻塞,或者在超出指定超时后结束。如果超出指定超时,则返回null.如果指定超时为0,意味着将无限期地等待。

代码优化技巧:

异常优化:永远不要在循环中处理异常,循环构造异常栈会十分耗时,把异常捕获放循环外面。

使用局部变量:局部变量存放在虚拟机栈的本地变量表中,本地变量表会随着方法销毁(出栈)而销毁,所以不需要gc。new出的对象存放在堆中,需要gc回收。而static变量存放于方法区,在编译时通过cinit构造生成,所以生命周期与类相同,方法区gc几乎不去回收(永久代),所以static多了会很耗费内存。

位运算:位运算速度是最快的,经常使用的除法可替换成>>,乘法可替换成<<,右移一位等同于除以2,左移一位等同于乘以2。

替换switch:if和switch性能区别并不大,但是有时使用if性能会更高,比如:

switch(num):
case 1:return 1;
case 2:return 2;
case 3:return 3
default:return -1


使用if优化后:

int swArr[3]={1,2,3};
if(num<1||num>3){
return -1;
}else{
return swArr[num];
}


由于对数组随机访问非常快,所以使用if要比switch快。这需要根据不同业务选择性优化。另外,使用策略或者工厂模式都可以优化swtich和if判断,方便解耦。

表达式:表达式运算是耗时的,可以在不影响业务的情况下把一些循环内的重复性的表达式提取到循环外用变量保存,然后再在循环内部使用。另外我们经常使用24*60*60这样的方式计算一天的秒数,其实可以在变量中直接写好计算结果。

展开循环:展开循环可参考如下代码:

未展开前:

int num[]=new int[10000];
for(int i=0;i<10000;i++){
num[i]=i;
}


展开后:

int num[]=new int[10000];
for(int i=0;i<10000;i+=3){
num[i]=i;
num[i+1]=i+1;
num[i+2]=i+2;
}


这种情况展开后要比展开前运算速度快,因为循环时减少了步进的判断。

使用布尔运算代替位运算:位运算虽然快,也存在位逻辑,但是在判断时使用位运算和其他逻辑运算一起时,java的if会完成位运算的判断执行后再继续判断条件中的其他逻辑运算。而布尔运算在条件满足后会直接跳转到if块中执行,省略后续的逻辑运算。不过通常我们只用布尔运算。

优化数组拷贝:使用System.arrayCopy(),因为他是native的,调用操作系统实现的拷贝,效率非常高。

使用缓冲区:BufferedInput和BufferedOutput在上面的文章中已经介绍过了,同样BufferedWrtier和BufferedReader效率也非常高。优先使用缓冲区。

使用静态方法:静态方法不需要构建实例就可以直接使用,并且由于方法区gc很少回收,且jvm会缓存常用的类,所以一些常用工具类封装成static的性能会更高。而且要比函数重载更具有表达意义。

使用设计模式:在对象比较大时可以使用原型模式替换new操作,尤其对象构造函数比较耗时时,可以直接使用原型模式clone对象,也可以使用apache的commons下的BeanUtil中的clone方法。同样在一些业务下,可以使用单例模式、享元模式、代理模式、工厂模式等常用的设计模式优化对象生成过程,提升性能。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: