本文来自kid_2412的博客.
程序代码优化要点:
- 字符串优化:分析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区别:
- Buffer和Channel:
- Buffer原理:
- API:
- 零拷贝:
引用类型:
- 强引用:直接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性能会更高,比如:
<span style="font-size:18px;"><code class="hljs cs has-numbering"><span class="hljs-keyword">switch</span>(num):<span class="hljs-keyword">case</span> <span class="hljs-number">1</span>:<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>;
<span class="hljs-keyword">case</span> <span class="hljs-number">2</span>:<span class="hljs-keyword">return</span> <span class="hljs-number">2</span>;
<span class="hljs-keyword">case</span> <span class="hljs-number">3</span>:<span class="hljs-keyword">return</span> <span class="hljs-number">3</span>
<span class="hljs-keyword">default</span>:<span class="hljs-keyword">return</span> -<span class="hljs-number">1</span></code></span><ul style="" class="pre-numbering"><li><span style="font-size:18px;">1</span></li><li><span style="font-size:18px;">2</span></li><li><span style="font-size:18px;">3</span></li><li><span style="font-size:18px;">4</span></li><li><span style="font-size:18px;">5</span></li></ul>
使用if优化后:
<span style="font-size:18px;"><code class="hljs cs has-numbering"><span class="hljs-keyword">int</span> swArr[<span class="hljs-number">3</span>]={<span class="hljs-number">1</span>,<span class="hljs-number">2</span>,<span class="hljs-number">3</span>};<span class="hljs-keyword">if</span>(num<<span class="hljs-number">1</span>||num><span class="hljs-number">3</span>){
<span class="hljs-keyword">return</span> -<span class="hljs-number">1</span>;
}<span class="hljs-keyword">else</span>{
<span class="hljs-keyword">return</span> swArr[num];
}</code></span><ul style="" class="pre-numbering"><li><span style="font-size:18px;">1</span></li><li><span style="font-size:18px;">2</span></li><li><span style="font-size:18px;">3</span></li><li><span style="font-size:18px;">4</span></li><li><span style="font-size:18px;">5</span></li><li><span style="font-size:18px;">6</span></li></ul>
由于对数组随机访问非常快,所以使用if要比switch快。这需要根据不同业务选择性优化。另外,使用策略或者工厂模式都可以优化swtich和if判断,方便解耦。
- 表达式:表达式运算是耗时的,可以在不影响业务的情况下把一些循环内的重复性的表达式提取到循环外用变量保存,然后再在循环内部使用。另外我们经常使用24*60*60这样的方式计算一天的秒数,其实可以在变量中直接写好计算结果。
- 展开循环:展开循环可参考如下代码:
未展开前:
<span style="font-size:18px;"><code class="hljs cs has-numbering"><span class="hljs-keyword">int</span> num[]=<span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">10000</span>];<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i=<span class="hljs-number">0</span>;i<<span class="hljs-number">10000</span>;i++){
num[i]=i;
}</code></span><ul style="" class="pre-numbering"><li><span style="font-size:18px;">1</span></li><li><span style="font-size:18px;">2</span></li><li><span style="font-size:18px;">3</span></li><li><span style="font-size:18px;">4</span></li></ul>
展开后:
<span style="font-size:18px;"><code class="hljs cs has-numbering"><span class="hljs-keyword">int</span> num[]=<span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">10000</span>];<span class="hljs-keyword">for</span>(<span class="hljs-keyword">int</span> i=<span class="hljs-number">0</span>;i<<span class="hljs-number">10000</span>;i+=<span class="hljs-number">3</span>){
num[i]=i;
num[i+<span class="hljs-number">1</span>]=i+<span class="hljs-number">1</span>;
num[i+<span class="hljs-number">2</span>]=i+<span class="hljs-number">2</span>;
}</code></span><ul style="" class="pre-numbering"><li><span style="font-size:18px;">1</span></li><li><span style="font-size:18px;">2</span></li><li><span style="font-size:18px;">3</span></li><li><span style="font-size:18px;">4</span></li><li><span style="font-size:18px;">5</span></li><li><span style="font-size:18px;">6</span></li></ul>
这种情况展开后要比展开前运算速度快,因为循环时减少了步进的判断。
- 使用布尔运算代替位运算:位运算虽然快,也存在位逻辑,但是在判断时使用位运算和其他逻辑运算一起时,java的if会完成位运算的判断执行后再继续判断条件中的其他逻辑运算。而布尔运算在条件满足后会直接跳转到if块中执行,省略后续的逻辑运算。不过通常我们只用布尔运算。
- 优化数组拷贝:使用System.arrayCopy(),因为他是native的,调用操作系统实现的拷贝,效率非常高。
- 使用缓冲区:BufferedInput和BufferedOutput在上面的文章中已经介绍过了,同样BufferedWrtier和BufferedReader效率也非常高。优先使用缓冲区。
- 使用静态方法:静态方法不需要构建实例就可以直接使用,并且由于方法区gc很少回收,且jvm会缓存常用的类,所以一些常用工具类封装成static的性能会更高。而且要比函数重载更具有表达意义。
- 使用设计模式:在对象比较大时可以使用原型模式替换new操作,尤其对象构造函数比较耗时时,可以直接使用原型模式clone对象,也可以使用apache的commons下的BeanUtil中的clone方法。同样在一些业务下,可以使用单例模式、享元模式、代理模式、工厂模式等常用的设计模式优化对象生成过程,提升性能。