Java性能优化的十条小技巧

语言: CN / TW / HK

1 System.nanoTime

测试性能时, System.nanoTimeSystem.currentTimeMills 更精确,前者使用纳秒计时,且对系统影响更小。

具体来说:

  • System.currentTimeMills 返回自 1970年1月1日 以来经过的毫秒数,返回的精度与操作系统有关
  • System.nanoTime :不是现实时间,是虚拟机提供的计时时间,精确到纳秒

2 ThreadLocalRandom

通常生成随机数会使用 Random 类, Random 是线程安全的, Random 实例里面有一个原子性的种子变量来记录当前种子的值,当要生成新的随机数时,会根据当前种子计算新的种子并更新回原子变量。多线程下计算新种子,会竞争同一个原子变量的更新操作,会造成大量线程进行自旋测试,降低并发性能。

ThreadLocalRandom 在当前线程维护了一个种子,适合在多线程场景下提供高性能的伪随机数生成,使用如下:

ThreadLocalRandom random = ThreadLocalRandom.current();
random.nextInt(range);

3 使用局部变量

理论上说,访问局部变量会快于类变量,因为局部变量保存在方法栈中,而类变量保存在堆中。如果在某个类方法中需要多次访问类变量,建议先创建一个局部变量并使其具有与类变量相同的值。

4 关于正则表达式替换

由于正则表达式替换时每次都需要编译正则表达式到一个中间结构,因此比常规的直接替换要慢,如果是固定的正则表达式替换,可以采用预编译的思想:

Pattern pattern = Pattern.compile("origin str");
public String replace(String str){
    return pattern.matcher(str).replaceAll("target str");
}

而不是采用:

public String replace(String str){
    return str.replace("origin str","target str");
}

5 关于字符串拼接

尽可能使用如下形式:

String a = "xxx";
String b = "xxx";
String c = new StringBuilder().append(a).append(b).toString();

性能相对不好的是如下情形(得益于 JVM 默认开启字符串拼接优化):

String c = a+b;

性能最差的是:

StringBuilder c = new StringBuilder();
c.append(a);
c.append(b);
String result = c.toString();

因为这样 JIT 不会优化。

另外,在无关线程安全的情况下,尽可能使用 StringBuilder 而不是 StringBuffer

6 关于数字转字符串

intString 是一个较为耗时的操作,尽量避免不必要的转化,如果确实需要,可以预先将一批 int 转为 String ,需要的时候直接取出:

public static class CommonUtil{
    static int cacheSize = 1024;
    static String [] caches = new String[cacheSize];
    static{
        for(int i=0;i<cacheSize;++i){
            caches[i] = String.valueOf(i);
        }
    }
    public static String int2String(int data){
        if(data < cacheSize){
            return caches[size];
        }else{
            return String.valueOf(data);
        }
    }
}

这样相比起直接使用

Stirng.valueOf(data)

性能会高一点。

7 switch / if

少分支的情况下,建议使用 if ,多分支建议使用 switch ,常用的“少分支”标准是 2-5个

8 采用返回码而不是抛异常

除非必要使用异常,应该避免把正常的返回错误结果使用异常来代替。抛异常会导致性能是因为构造异常对象时需要一个填写异常栈的过程,就是 Throwable 中的 fillInStackTrace ,这是一个 Native 方法,会填写异常栈,造成较为严重的耗时。

一种优化方法是,自定义异常,重写 fillInStackTrace()

public class MyException extends RuntimeException{
    ...
    public synchronized Throwable fillInStackTrace(){
        this.setStackTrace(new StackTraceElement[0]);
        return this;
    }
}

另外, JVM 会对频繁抛出的异常做 Fast Throw 优化,如果检测到代码中某一位置连续多次抛出同一类型的异常,则采用 Fast Throw 方式,异常栈信息不会被填写,这种异常抛出速度很快,因为不需要在堆里分配内存,也不需要构造完整的异常栈信息,默认对如下异常采用 Fast Throw 优化:

NullPointerException
ArithmeticException
ArrayIndexOutOfBoundsExpcetion
ArrayStoreException
ClassCastException

需要注意的是, Fast Throw 虽然提高了性能,但是会导致异常栈消息,从而无法快速定位到错误代码,如果需要避免异常栈优化,可以使用参数:

-XX:-OmitStackTraceInFastThrow

9 位运算

可以通过位运算代替部分算术运算以提高性能,比如:

(a & 1) == 1
(a & 1) == 0
a>>1
a<<1

10 其他技巧

  • 字符串搜索等需要搜索单个字符时,使用 String.indexOf(char) 而不是 String.indexOf(String)
  • 对于判断一些特殊的 ID ,比如长度 9位 且以 11 开头,可以直接使用常数判断: id>=110_000_000 && id<=120_000_000 ,而不需要通过 String.valueOf 转为字符串再通过 String.length + String.startWith 判断
  • switch 中,可以使用 int 去代替 String
  • 日志输出可以直接使用字符串拼接而不是 模板+{} ,因为会有一个占位符 {} 替换成目标变量的耗时过程,被频繁调用的话建议直接字符串拼接
  • 传输的实体类尽量避免使用 String ,因为其中涉及序列化、反序列化、字符串构造,而对于 byte[] 构造 String 的方法,内部会调用 StringCoding.decode ,相比起通过 char[] / Stirng 构造会造成更大的耗时
分享到: