在 JAVA 程序中,性能问题的大部分原因并不在于 JAVA 语言,而是程序 本身。养成良好的编码习惯非常重要,能够显著地提升程序性能。 1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并 不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面: 控制资源的使用,通过线程同步来控制资源的并发访问; 控制实例的产生,以达到节约资源的目的; 控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线 程之间实现通信。 2. 尽量避免随意使用静态变量 当某个对象被定义为 static 变量所引用,那么 GC 通常是不会回收这个对象 所占有的内存,如下图所示 此时静态变量 b 的生命周期与 A 类同步,如果 A 类不会卸载,那么 b 对象 会常驻内存,直到程序终止。 3. 尽量避免过多过常地创建 Java 对象 尽量避免在经常调用的方法,循环中 new 对象,由于系统不仅要花费时间 来创建对象,而且还要花时间对这些对象进行垃圾回收和处理,在我们可 以控制的范围内,最大限度地重用对象,最好能用基本的数据类型或数组 来替代对象。 4. 尽量使用 final 修饰符 带有 final 修饰符的类是不可派生的。在 JAVA 核心 API 中,有许多应用 final 的例子,例如 java、lang、String,为 String 类指定 final 防止了使用者 覆盖 length()方法。另外,如果一个类是 final 的,则该类所有方法都是 final 的。java 编译器会寻找机会内联(inline)所有的 final 方法(这和具体的 编译器实现有关),此举能够使性能平均提高 50%。 如:让访问实例内变量的 getter/setter 方法变成”final: 简单的 getter/setter 方法应该被置成 final,这会告诉编译器,这个方法不 会被重载,所以,可以变成”inlined”,例子: 5. 尽量使用局部变量 调 用 方法 时传 递的 参数 以及 在调 用中 创建的 临 时变 量都 保存 在栈 (Stack)中,速度较快;其他变量,如静态变量、实例变量等,都在堆(Heap) 中创建,速度较慢。 6. 尽量处理好包装类型和基本类型两者的 使用场所 虽然包装类型和基本类型在使用过程中是可以相互转换,但它们两者所产 生的内存区域是完全不同的,基本类型数据产生和处理都在栈中处理,包 装类型是对象,是在堆中产生实例。在集合类对象,有对象方面需要的处 理适用包装类型,其他的处理提倡使用基本类型。 7. 慎用 synchronized,尽量减小 synchronize 的方法 都知道,实现同步是要很大的系统开销作为代价的,甚至可能造成死锁, 所以尽量避免无谓的同步控制。synchronize 方法被调用时,直接会把当前 对象锁了,在方法执行完之前其他线程无法调用当前对象的其他方法。所 以,synchronize 的方法尽量减小,并且应尽量使用方法同步代替代码块同 步。 8 尽量不要使用 finalize 方法 实际上,将资源清理放在 finalize 方法中完成是非常不好的选择,由于 GC 的工作量很大,尤其是回收 Young 代内存时,大都会引起应用程序暂停, 所以再选择使用 finalize 方法进行资源清理,会导致 GC 负担更大,程序运 行效率更差。 9 尽量使用基本数据类型代替对象 String str = "hello"; 上面这种方式会创建一个“hello”字符串,而且 JVM 的字符缓存池还会 缓存这个字符串; String str = new String("hello"); 此时程序除创建字符串外,str 所引用的 String 对象底层还包含一个 char[] 数组,这个 char[]数组依次存放了 h,e,l,l,o 10 多线程在未发生线程安全前提下应尽量 使用 HashMap、ArrayList HashTable、Vector 等使用了同步机制,降低了性能。 11 尽量合理的创建 HashMap 当你要创建一个比较大的 hashMap 时,充分利用这个构造函数 public HashMap(int initialCapacity, float loadFactor); 避免 HashMap 多次进行了 hash 重构,扩容是一件很耗费性能的事,在默 认中 initialCapacity 只有 16,而 loadFactor 是 0.75,需要多大的容量, 你最好能准确的估计你所需要的最佳大小,同样的 Hashtable,Vectors 也是一样的道理。 12 尽量减少对变量的重复计算 13 尽量避免不必要的创建 14 尽量在 finally 块中释放资源 程序中使用到的资源应当被释放,以避免资源泄漏,这最好在 finally 块中去做。 不管程序执行的结果如何,finally 块总是会执行的,以确保资源的正确关闭。 15 尽量使用移位来代替'a/b'的操作 16 尽量使用移位来代替'a*b'的操作 17 尽量确定 StringBuffer 的容量 StringBuffer 的构造器会创建一个默认大小(通常是 16)的字符数组。在 使用中,如果超出这个大小,就会重新分配内存,创建一个更大的数组, 并将原先的数组复制过来,再丢弃旧的数组。在大多数情况下,你可以在 创建 StringBuffer 的时候指定大小,这样就避免了在容量不够的时候自动 增长,以提高性能。 如: StringBuffer buffer = new StringBuffer(1000); 18 尽量早释放无用对象的引用 大部分时,方法局部引用变量所引用的对象会随着方法结束而变成垃圾, 因此,大部分时候程序无需将局部,引用变量显式设为 null。 例如: Java 代码 上面这个就没必要了,随着方法 test()的执行完成,程序中 obj 引用变量的 作用域就结束了。但是如果是改成下面: Java 代码 这时候就有必要将 obj 赋值为 null,可以尽早的释放对 Object 对象的引用。 19. 尽量避免使用二维数组 二维数据占用的内存空间比一维数组多得多,大概 10 倍以上 20. 尽量避免使用 split 除非是必须的,否则应该避免使用 split,split 由于支持正则表达式,所以 效率比较低,如果是频繁的几十,几百万的调用将会耗费大量资源,如果 确 实 需 要 频 繁 的 调 用 split , 可 以 考 虑 使 用 apache 的 StringUtils.split(string,char),频繁 split 的可以缓存结果。 21. ArrayList & LinkedList 一个是线性表,一个是链表,一句话,随机查询尽量使用 ArrayList, ArrayList 优于 LinkedList,LinkedList 还要移动指针,添加删除的操作 LinkedList 优于 ArrayList,ArrayList 还要移动数据,不过这是理论性分析, 事实未必如此,重要的是理解好 2 者得数据结构,对症下药。 22. 尽量使用System.arraycopy ()代替通过来 循环复制数组 System.arraycopy() 要比通过循环来复制数组快的多。 23. 尽量缓存经常使用的对象 尽可能将经常使用的对象进行缓存,可以使用数组,或 HashMap 的容器 来进行缓存,但这种方式可能导致系统占用过多的缓存,性能下降,推荐 可以使用一些第三方的开源工具,如 EhCache,Oscache 进行缓存,他们 基本都实现了 FIFO/FLU 等缓存算法。 24. 尽量避免非常大的内存分配 有时候问题不是由当时的堆状态造成的,而是因为分配失败造成的。分配 的内存块都必须是连续的,而随着堆越来越满,找到较大的连续块越来越 困难。 25. 慎用异常 当创建一个异常时,需要收集一个栈跟踪(stack track),这个栈跟踪用于描 述异常是在何处创建的。构建这些栈跟踪时需要为运行时栈做一份快照, 正是这一部分开销很大。当需要创建一个 Exception 时,JVM 不得不说: 先别动,我想就您现在的样子存一份快照,所以暂时停止入栈和出栈操作。 栈跟踪不只包含运行时栈中的一两个元素,而是包含这个栈中的每一个元 素。 如果您创建一个 Exception ,就得付出代价,好在捕获异常开销不大,因 此可以使用 try-catch 将核心内容包起来。从技术上讲,你甚至可以随意 地抛出异常,而不用花费很大的代价。招致性能损失的并不是 throw 操作 ——尽管在没有预先创建异常的情况下就抛出异常是有点不寻常。真正要 花代价的是创建异常,幸运的是,好的编程习惯已教会我们,不应该不管 三七二十一就抛出异常。异常是为异常的情况而设计的,使用时也应该牢 记这一原则。 26. 尽量重用对象 特别是 String 对象的使用中,出现字符串连接情况时应使用 StringBuffer 代替,由于系统不仅要花时间生成对象,以后可能还需要花时间对这些对 象进行垃圾回收和处理。因此生成过多的对象将会给程序的性能带来很大 的影响。 27. 不要重复初始化变量 默认情况下,调用类的构造函数时,java 会把变量初始化成确定的值,所 有的对象被设置成 null,整数变量设置成 0,float 和 double 变量设置成 0.0,逻辑值设置成 false。当一个类从另一个类派生时,这一点尤其应该注 意,因为用 new 关键字创建一个对象时,构造函数链中的所有构造函数都 会被自动调用。 这里有个注意,给成员变量设置初始值但需要调用其他方法的时候,最好 放在一个方法。比如 initXXX()中,因为直接调用某方法赋值可能会因为类 尚未初始化而抛空指针异常,如:public int state = this.getState()。 28. 通过StringBuffer的构造函数来设定它的 初始化容量,可以明显提升性能 StringBuffer 的默认容量为 16,当 StringBuffer 的容量达到最大容量时, 它会将自身容量增加到当前的 2 倍+2,也就是 2*n+2。无论何时,只要 StringBuffer 到达它的最大容量,它就不得不创建一个新的对象数组,然 后复制旧的对象数组,这会浪费很多时间。所以给 StringBuffer 设置一个 合理的初始化容量值,是很有必要的! 29. 合理使用 java.util.Vector 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)。 30.不用 new 关键字创建对象的实例 31 对 于 常 量 字 符 串 , 用 'String' 代 替 'StringBuffer' 32 在 finally 块中关闭 Stream 程序中使用到的资源应当被释放,以避免资源泄漏。这最好在 finally 块中 去做。不管程序执行的结果如何,finally 块总是会执行的,以确保资源的 正确关闭。 33. 使用'System.arraycopy ()'代替通过来循环复制数组 34. 在字符串相加的时候,使用 ' ' 代替 " ",如果该字符串只有一个字符的话 35 为'Vectors' 和 'Hashtables'定义初始大小 36 避免在循环条件中使用复杂表达式 37HaspMap 的遍历 38 尽量避免 BigInteger 和 BigDecimal 我们来看下 BigInteger 和 BigDecimal。尤其是后者,由于其精度高而受欢迎。但这是有代 价的。 BigInteger 和 BigDecimal 比简单的 long 或 double 需要更多的内存,并且大大降低所有的 计算速度。因此,如果你需要额外的精度,或者你的数字超过了一个 long 范围,最好三思 而后行。这可能是你在提升性能问题中唯一需要更改的地方,特别是当你正在实现一个数学 算法。 39 array(数组)和 ArrayList 的使用 array 数组效率最高,但容量固定,无法动态改变,ArrayList 容量可以动态增 长,但牺牲了效率。 40 单线程应尽量使用 HashMap, ArrayList, 除非必要,否则不推荐使用 HashTable,Vector,它们使用了同步机制,而 降低了性能 41 尽量使用基本数据类型代替对象。 42 避免枚举,浮点数的使用 43 考虑使用静态方法,如果你没有必要去 访问对象的外部,那么就使你的方法成为静 态方法。它会被更快地调用,因为它不需要 一个虚拟函数导向表。这同时也是一个很好 的实践,因为它告诉你如何区分方法的性 质,调用这个方法不会改变对象的状态。 44 尽量删除多余代码 45 尽量精简代码