浅谈Java优化

代码优化的目标

  • 减小代码体积
  • 提高代码运行效率

代码优化细节

尽量指定类、方法的final修饰符

带有 final 修饰符的类是不可派生的。在 Java 核心 API 中,有许多应用 final 的例子,如 java.lang.String ,整个类都是 final 的。为类指定 final 修饰符可以让类不可以被继承,为方法指定 final 修饰符可以让方法不可以被重写。如果指定了一个类为 final ,则该类所有的方法都是 final 的。Java 编译器会寻找机会内联所有的 final 方法,内联对于提升 Java 运行效率作用重大,具体参见 Java 运行期优化。此举能够使性能平均提升50%。

尽量重用对象

特别是 String 对象的使用,出现字符串连接时应该使用 StringBuilder、StringBuffer 代替。由于 Java 虚拟机不仅要花时间生成对象,以后可能还需要花时间对这些对象进行垃圾回收和处理,因此,生成过多的对象将会给程序的性能带来很大的影响。

尽可能使用局部变量

调用方法时传递的参数以及在调用中差UN关键的临时变量都保存在占中,速度较快;其他变量,如静态变量、实例变量等,都在堆中创建,速度较慢。另外,栈中创建的变量,随着方法的运行结束,这些内容就没了,不需要额外的垃圾回收。

及时关闭流

Java 编程过程中,进行数据库连接、I/O 流操作时务必小心,在使用完毕后,及时关闭以释放资源。因为对这些大对象的操作会造成系统很大的开销,稍有不慎,将会导致严重后果。

进来能够减少对变量的重复计算

明确一个概念,对方法的调用,即使方法中只有一个语句,也是有消耗的,包括创建栈帧、调用方法是保护现场、调用方法完毕时恢复现场等。所以例如下面的操作:

1
2
for (int i = 0; i < list.size(); i++)
{...}

建议替换为:

1
2
for (int i = 0, int length = list.size(); i < length; i++)
{...}

这样,在list.size()很大的时候,就减少了许多消耗。

尽量采用懒加载的策略,即在需要时才创建

例如:

1
2
3
4
String str = "abcde";
if (i == 1){
list.add(str);
}

可替换为:

1
2
3
4
if (i == 1){
String str = "abcde";
list.add(str);
}

慎用异常

异常对性能不利。抛出异常首先要创建一个新的对象,Throwable接口的构造函数调用名为 fillInStackTrace() 的本地同步方法, fillInStackTrace() 方法检查堆栈,收集调用跟踪信息。只要有异常被抛出,Java 虚拟机就必须调整调用堆栈,因为在处理过程中创建了一个新的对象。异常只能用于错误处理,不应该用来控制程序流程。

不要在循环中使用try-catch,应该将其放在最外层,除非不得已。

关于内容长度

如果能估计到待添加的内容长度,为底层以数组方式实现的集合、工具类指定初始长度,比如:ArrayList、StringBuilder、StringBuffer、HashMap、HashSet等。

以StringBuilder为例:

  • StringBuilder() //默认分配16个字符的空间
  • StringBuilder(int size) //默认分配size个字符的空间
  • StringBuilder(String str) //默认分配16+str.length()个字符的空间

可以通过类(这里指的不仅仅是上面的 StringBuilder)来设定他的初始化变容量,这样可以明显地提升性能。以 StringBuilder 为例,length 表示当前的 StringBuilder 能保持的字符数量。因为当 StringBuilder 达到最大容量时,会将自身容量增加到当前的2倍再加2,无论何时只要 StringBuilder 达到他的最大容量,就不得不创建一个新的字符数组然后将旧的字符数组内容拷贝到新字符数组中——然鹅这是十分耗费性能的操作。那么,如果能预估到字符数组中要存放5000个字符而不指定长度,最接近5000的2次幂是4096,忽略每次扩容加的2,那么:

  • 在4096基础上,在申请8194个大小的字符数组,加起来相当于一次申请了12290个大小的字符数组,如果一开始就能指定5000个大小的字符数组,就能节省一倍以上的空间。
  • 把原来的4096个字符拷贝到新的字符数组中去,又浪费了性能。

会发现,如果不指定字符长度,既浪费内存空间又降低代码运行效率。所以,给底层以数组实现的集合、工具类设置一个合理的初始化容量是代码优化很重要的一部分。但是注意像 HashMap 这种是以数组+链表实现的集合,别把初始大小和估计的大小设置的一样,因为一个 table 上只连接一个对象的可能性几乎为0。初始大小建议设置为2的N次幂,如果能估计到有2000个元素,设置成 new HashMap(128)、new HashMap(256)都是可以的。

复制

复制大量数据时,使用 System.arraycopy()命令。

乘法和除法使用移位操作

如下:

1
2
3
4
for (val = 0; val < 100000; val += 5) {
a = val * 8;
b = val / 2;
}

用移位操作可以极大地提升性能,因为在计算机底层,对位的操作是最方便、最快的。因此修改为:

1
2
3
4
for (val = 0; val < 100000; val +=5) {
a = val << 3;
b = val >> 3;
}

移位操作虽然快,但是可能会使得代码不好理解,因此如果使用最好加上相应的注释。

循环内不要不断创建对象引用

例如:

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

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

1
2
3
4
Object obj = null;
for (int i = 0; i <= count; i++) {
obj = new Object();
}

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

关于 array 和 ArrayList

基于效率和类型检查的考虑,应尽可能使用 array,无法确定数组大小时才使用 ArrayList。

集合类型选择

尽量使用 HashMap、ArrayList、StringBuilder,除非线程安全需要,否则不推荐使用 Hashtable、Vector、StringBuffer,后三者由于使用同步机制而导致了性能开销。

不要将数组声明为 public,static,final

因为毫无意义。这样只是定义了引用为 static、final,数组的内容还是可以随意改变的,将数组声明为 public 更是一个安全漏洞,这意味着这个数组可以被外部类所改变。

进来那个在合适的场合使用单例

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

  • 控制资源的使用,通过线程同步来控制资源的并发访问。
  • 控制实例的产生,以达到节约资源的目的。
  • 控制数据的共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信。

尽量避免随意使用静态变量

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

1
2
3
public class A {
private static B b = new B();
}

此时静态变量 b 的生命周期与 A 类相同,如果 A 类不被卸载,那么引用 B 指向的 B 对象会常驻内存,知道程序终止。

及时清除不再需要的会话

为了清除不再活动的会话,许多应用服务器都有默认的会话超时时间,一般为30分钟。当应用服务器需要保存更多的会话时,如果内存不足,那么操作系统会把部分数据转移到磁盘,应用服务器也可能根据 MRU(最近频繁使用)算法把不分不活跃的会话转储到磁盘,那么必须要先被序列化,在大规模集群中,对对象进行序列化的代价是很昂贵的。因此,当会话不再需要时,应当及时调用 HttpSession 的 invalidate() 方法清楚会话。

实现 RandomAccess 接口的集合比如 ArrayList,应当使用最普通的 foreach 循环来遍历

这是 JDK 推荐给用户的。JDK API 对于 RandomAccess 接口的解释是:实现 RandowAccess 接口用来表明其支持快速随机访问,此接口的主要目的是允许一般算法更改其行为,从而将其应用到随机或连续访问列表式能提供良好的性能。实际经验表明,实现 RandomAccess 接口的类实例,假如是随机访问的,使用普通 for 循环效率将高于使用 foreach 循环;反过来,如果是顺序访问的,则使用 Iterator 效率更高。可以使用类似如下的代码做判断:

1
2
if (list instanceof RandomAccess) {
}

foreach 循环的底层实现原理就是迭代器 Iterator ;可变长度参数以及 foreach 循环原理。所以后半句“反过来,如果是顺序访问的,则使用 Iterator 效率会更高”的意思就是顺序访问的那些类实例,使用 foreach 循环去遍历。

使用同步代码块替代同步方法

这点在多线程模块中的synchronized锁方法块一文中已经讲得很清楚了,除非能确定一整个方法都是需要进行同步的,否则尽量使用同步代码块,避免对那些不需要进行同步的代码也进行了同步,影响了代码执行效率。

将常量声明为static final,并以大写命名

这样在编译期间就可以把这些内容放入常量池中,避免运行期间计算生成常量的值。另外,将常量的名字以大写命名也可以方便区分出常量与变量。

不要创建一些不使用的对象,不要导入一些不使用的类

这毫无意义,如果代码中出现“The value of the local variable i is not used”、“The import java.util is never used”,那么请删除这些无用的内容。

程序运行过程中避免使用反射

反射是Java提供给用户一个很强大的功能,功能强大往往意味着效率不高。不建议在程序运行过程中使用尤其是频繁使用反射机制,
特别是Method的invoke方法,如果确实有必要,一种建议性的做法是将那些需要通过反射加载的类在项目启动的时候通过反射实例化出一个对象并放入内存——用户只关心和对端交互的时候获取最快的响应速度,并不关心对端的项目启动花多久时间。

使用数据库连接池和线程池

这两个池都是用于重用对象的,前者可以避免频繁地打开和关闭连接,后者可以避免频繁地创建和销毁线程。

使用带缓冲的输入输出流进行IO操作

带缓冲的输入输出流,即 BufferedReader、BufferedWriter、BufferedInputStream、BufferedOutputStream,这可以极大地提升IO效率。

ArrayList和LinkedList

顺序插入和随机访问比较多的场景使用ArrayList,元素删除和中间插入比较多的场景使用LinkedList这个,理解ArrayList和LinkedList的原理就知道了。

不要让public方法中有太多的形参

public方法即对外提供的方法,如果给这些方法太多形参的话主要有两点坏处:

  • 违反了面向对象的编程思想,Java讲求一切都是对象,太多的形参,和面向对象的编程思想并不契合
  • 参数太多势必导致方法调用的出错概率增加
    至于这个”太多”指的是多少个,3、4个吧。比如我们用JDBC写一个insertStudentInfo方法,有10个学生信息字段要插如Student表中,可以把这10个参数封装在一个实体类中,作为insert方法的形参。

字符串变量和字符串常量equals的时候将字符串常量写在前面

这是一个比较常见的小技巧了,如果有以下代码:

1
2
3
4
String str = "119"; 
if (str.equals("119")) {
...
}

建议修改为:

1
2
3
4
String str = "119"; 
if ("119".equals(str)) {
...
}

不要对数组使用 toString()方法

看一下对数组使用toString()打印出来的是什么:

1
2
3
4
public static void main(String[] args) {
int[] is = new int[]{111, 123, 234};
System.out.println(is.toString());
}

本意是想打印出数组内容,却有可能因为数组引用is为空而导致空指针异常。不过虽然对数组toString()没有意义,但是对集合 toString()是可以打印出集合里面的内容的,因为集合的父类AbstractCollections重写了Object的 toString()方法。

公用的集合类中不使用的数据一定要及时remove掉

如果一个集合类是公用的(也就是说不是方法里面的属性),那么这个集合里面的元素是不会自动释放的,因为始终有引用指向它们。所以,如果公用集合里面的某些数据不使用而不去remove掉它们,那么将会造成这个公用集合不断增大,使得系统有内存泄露的隐患。

打赏
  • © 2020 Shadowalker
  • Powered by Hexo Theme Ayer
    • PV:
    • UV:

请我喝杯咖啡吧~

支付宝
微信