Java程序性能优化(一)

养成良好的编码习惯提升程序性能

1. 在合适的场合使用单例

使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:

1.1控制资源的使用,通过线程同步来控制资源的并发访问;

1.2控制实例的产生,以达到节约资源的目的;

1.3控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。

2. 避免随意使用静态变量

当某个对象被定义为static变量所引用,那么GC通常是不会回收这个对象所占有的内存,如:

public class A{

private static B b = new B();

}

此时静态变量b的生命周期与A类同步,如果A类不会卸载,那么b对象会常驻内存,直到程序终止。

3. 尽量使用final修饰符 

带有final修饰符的类是不可派生的。在JAVA核心API中,有许多应用final的例子,例如java、lang、String,为String类指定final防止了使用者覆盖length()方法。另外,如果一个类是final的,则该类所有方法都是final的。java编译器会寻找机会内联(inline)所有的final方法(这和具体的编译器实现有关),此举能够使性能平均提高50%。

如:让访问实例内变量的getter/setter方法变成”final:

简单的getter/setter方法应该被置成final,这会告诉编译器,这个方法不会被重载,所以,可以变成”inlined”,例子:

class MAF {

public void setSize (int size) {

_size = size;

}

private int _size;

}

更正:

class DAF_fixed {

final public void setSize (int size) {

_size = size;

}

private int _size;

}

4. 静态类、单例类、工厂类将它们的构造函数置为 private

这是因为静态类、单例类、工厂类这种类本来我们就不需要外部将它们 new 出来,将构造函数置为 private 之后,保证了这些类不会产生实例对象。

5. 尽量使用局部变量

调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度较快;其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。

6. 处理好包装类型和基本类型两者的使用场所尽量使用基本数据类型代替对象

虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,包装类型是对象,是在堆中产生实例。什么时候该用包装类,什么时候该用基本类型,看基本的业务来定:这个字段允不允许null值,如果允许,则必然要用封装类;否则,基本类型就可以了。如果用到比如集合类对象,泛型和反射调用函数,就需要用包装类! 

String str = "hello";

上面这种方式会创建一个“hello”字符串,而且JVM的字符缓存池还会缓存这个字符串;

String str = new String("hello");

此时程序除创建字符串外,str所引用的String对象底层还包含一个char[]数组,这个char[]数组依次存放了h,e,l,l,o

7. 慎用synchronized

实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。synchronize方法被调用时,直接会把当前对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所以在保证线程安全的前提下,同步的范围应该尽可能的小,除非能确定一整个方法都是需要进行同步的,否则尽量使用同步代码块,避免对那些不需要进行同步的代码也进行了同步,影响了代码执行效率。

8. 尽量不要使用finalize方法

实际上,将资源清理放在finalize方法中完成是非常不好的选择,由于GC的工作量很大,选择使用finalize方法进行资源清理,会导致GC负担更大,程序运行效率更差。

finalize方法是Object提供的的实例方法,使用规则如下:

  1. 当对象不再被任何对象引用时,GC会调用该对象的finalize()方法
  2. finalize()是Object的方法,子类可以覆盖这个方法来做一些系统资源的释放或者数据的清理
  3. 可以在finalize()让这个对象再次被引用,避免被GC回收;但是最常用的目的还是做cleanup
  4. Java不保证这个finalize()一定被执行;但是保证调用finalize的线程没有持有任何user-visible同步锁。
  5. 在finalize里面抛出的异常会被忽略,同时方法终止。
  6. 当finalize被调用之后,JVM会再一次检测这个对象是否能被存活的线程访问得到,如果不是,则清除该对象。也就是finalize只能被调用一次;也就是说,覆盖了finalize方法的对象需要经过两个GC周期才能被清除。

9. 尽量避免使用二维数组

二维数据占用的内存空间比一维数组多得多,大概10倍以上。

10. 在不必要考虑线程安全前提下应尽量使用HashMap、ArrayListHashTable、Vector等使用了同步机制,降低了性能。

11. 合理的初始化集合或者工具类的大小

 比如ArrayList、LinkedLlist、StringBuilder、StringBuffer、HashMap、HashSet、Vector等等。当你要创建一个比较大的hashMap时,充分利用这个构造函数

public HashMap(int initialCapacity, float loadFactor);

避免HashMap多次进行了hash重构,扩容是一件很耗费性能的事,在默认中initialCapacity只有16,而loadFactor是 0.75,需要多大的容量,你最好能准确的估计你所需要的最佳大小。

StringBuffer 的构造器会创建一个默认大小(通常是16)的字符数组。在使用中,如果超出这个大小,它就会重新分配内存,会将自身容量增加到当前的2倍+2,也就是2*n+2;并将原先的数组复制过来,再丢弃旧的数组,这会浪费很多时间。在大多数情况下,你可以在创建 StringBuffer的时候指定大小,这样就避免了在容量不够的时候自动扩容,以提高性能。

它会将自身容量增加到当前的2倍+2,也就是2*n+2。无论何时,只要StringBuffer到达它的最大容量,它就不得不创建一个新的对象数组,然后复制旧的对象数组,这会浪费很多时间。所以给StringBuffer设置一个合理的初始化容量值,是很有必要的!如:StringBuffer buffer = new StringBuffer(1000);

Vector与StringBuffer类似,每次扩展容量时,所有现有元素都要赋值到新的存储空间中。Vector的默认存储能力为10个元素,扩容加倍。

vector.add(index,obj) 这个方法可以将元素obj插入到index位置,但index以及之后的元素依次都要向下移动一个位置(将其索引加 1)。除非必要,否则对性能不利。同样规则适用于remove(int index)方法,移除此向量中指定位置的元素。将所有后续元素左移(将其索引减 1)。返回此向量中移除的元素。所以删除vector最后一个元素要比删除第1个元素开销低很多。删除所有元素最好用removeAllElements()方法。

如果要删除vector里的一个元素可以使用 vector.remove(obj);而不必自己检索元素位置,再删除,如int index = indexOf(obj);vector.remove(index)。

12. 避免在循环条件中使用复杂表达式

如:for(int i=0;i

并且在循环中应该避免使用复杂的表达式,在循环中,循环条件会被反复计算,如果不使用复杂表达式,而使循环条件值不变的话,程序将会运行的更快。

13. 就近创建所需对象,尽量避免不必要的创建

A a = new A();

if(i==1){ list.add(a); }

应该改为:

if(i==1){ A a = new A(); list.add(a); }

14. 尽量在finally块中释放资源

程序中使用到的资源应当及时被释放,以避免资源泄漏,特别是对 I/O流,数据库连接等大对象的操作会造成系统大的开销,并且这些最好在finally块中去做。因为不管程序执行的结果如何,finally块总是会执行的,以确保资源的正确关闭。

15. 考虑不用new关键字创建对象的实例

用new关键词创建类的实例时,构造函数链中的所有构造函数都会被自动调用。但如果一个对象实现了Cloneable接口,我们可以调用它的clone()方法。clone()方法不会调用任何类构造函数。

下面是Factory模式的一个典型实现:

public static Credit getNewCredit(){

return new Credit();

}

改进后的代码使用clone()方法:

private static Credit BaseCredit = new Credit();

public static Credit getNewCredit(){

return (Credit)BaseCredit.clone();

}

16. 尽早释放无用或者过期对象的引用

很多时候,方法局部引用变量所引用的对象会随着方法结束而变成垃圾,因此,大部分时候程序无需将局部引用变量显式设为null。例如:

public void test(){

Object obj = new Object();

……

Obj=null;

}

上面这个就没必要了,随着方法test()的执行完成,程序中obj引用变量的作用域就结束了。但是如果是改成下面:

public void test(){

Object obj = new Object();

……

Obj=null;

//执行耗时,耗内存操作;或调用耗时,耗内存的方法

……

}

这时候就有必要将obj赋值为null,可以尽早的释放对Object对象的引用。

17. 使用最有效率的方式去遍历Map

遍历Map的方式有很多,通常场景下我们需要的是遍历Map中的Key和Value,那么推荐使用的、效率最高的方式是:

public static void main(String[] args) {

HashMap hm = new HashMap();

hm.put("111", "222");

Set> entrySet = hm.entrySet();

Iterator> iter = entrySet.iterator();

 while (iter.hasNext()) {

Map.Entry entry = iter.next();

System.out.println(entry.getKey() + "\t" + entry.getValue());

}

}

  如果你只是想遍历一下这个Map的key值,那用”Set keySet = hm.keySet();”会比较合适一些

18. 循环内尽量不要创建对象引用

for (int i = 1; i <= count; i++) { Object obj = new Object(); }

这种做法会导致内存中有count份Object对象引用存在,count很大的话,就耗费内存了,建议为改为:

Object obj = null;

for (int i = 0; i <= count; i++) { obj = new Object(); }

这样的话,内存中只有一份Object对象引用,每次new Object()的时候,Object对象引用指向不同的Object罢了,但是内存中只有一份,这样就大大节省了内存空间了。

19. ArrayList & LinkedList

一个是线性表,一个是链表,随机查询ArrayList优于LinkedList,LinkedList还要移动指针,添加删除的操作LinkedList优于ArrayList,ArrayList还要移动数据,不过这是理论性分析,事实未必如此,重要的是理解好两者的数据结构。

20. 缓存经常使用的对象

尽可能将经常使用的对象进行缓存,可以使用数组,或HashMap的容器来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐可以使用一些第三方的开源工具,如EhCache,Oscache,redis进行缓存,他们基本都实现了FIFO/FLU等缓存算法。

21. 慎用异常,规避继承自 RuntimeException 的运行时异常

当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照,正是这一部分开销很大。当需要创建一个 Exception 时,JVM 不得不说:先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元素。

如果您创建一个 Exception ,就得付出代价,好在捕获异常开销不大,因此可以使用 try-catch 将核心内容包起来。从技术上讲,你甚至可以随意地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要花代价的是创建异常。

异常处理效率低,RuntimeException 的运行时异常中绝大多数完全可以由程序员来规避,比如 ArithmeticException 可以通过判断除数是否为空来规避,NullPointerException 可以通过判断对象是否为空来规避,IndexOutOfBoundsException 可以通过判断数组/字符串长度来规避,ClassCastException 可以通过 instanceof 关键字来规避,ConcurrentModificationException 可以使用迭代器来规避。

22. 尽量使用System.arraycopy ()代替通过来循环复制数组

System.arraycopy() 要比通过循环来复制数组快的多。

public void test15() {

        Integer[] strArray = new Integer[1000];

        for (int i = 0; i < 1000; i++) {

            strArray[i] = i;

        }

        Integer[] strArray2 = new Integer[100];

        System.arraycopy(strArray, 50, strArray2, 50, 50);

        for (int i = 0, len = strArray2.length; i < len; i++) {

            System.out.print(" " + strArray2[i] + " ");

        }

  }

 

23. 在字符串相加的时候,如果字符串只有一个字符的话,使用 ' ' 代替 " "。

public void method(String s) {

String string = s + "d"

string = "abc" + "d"

}

更正:

将一个字符的字符串替换成' '

public void method(String s) {

String string = s + 'd'

string = "abc" + 'd'

}

24. 把一个基本数据类型转为字符串,基本数据类型.toString()是最快的方式、String.valueOf(数据)次之、数据+"" 最慢

  1、String.valueOf()方法底层调用了Integer.toString()方法,但是会在调用前做空判断.

  2、Integer.toString()方法就不说了,直接调用了.

  3、i + “”底层使用了StringBuilder实现,先用append方法拼接,再用toString()方法获取字符串.

 

 

你可能感兴趣的:(性能优化)