JAVA开发中,大部分的性能问题原因并不在于JAVA语言本身,而是我们用这些语言写的程序,所以养成良好的编码习惯非常重要。

下面给大家分享一些日常开发中比较常见的典型案例:

一. 类中的内部方法声明为private

很多同学觉得这个无所谓,写代码时喜欢一个类里的所有方法都是public的(原因大家都懂),美其名曰:便于后期扩展。。

对于不需要外部访问的方法改为私有的,不仅仅是因为面向对象的思想,符合数据封装和安全访问原则,还有一个很大的好处就是任何private方法都是隐性的final!

Any private methods in a class are implicitly final.

《Think In Java》

摘自《Think In Java》第6章

被final修饰的方法能够增加内联inline的可能性

方法内联就是把调用方函数代码”复制”到调用方函数内部,作为自己的一部分代码执行,减少因函数调用产生的开销,即JIT优化。

伪代码如下:

public int sum(int a, int b, int c, int d){
    return add1(a, b) + add2(c, d);
}

private int add1(int a, int b){
    return a + b;
}

private int add2(int c, int d){
    return c + d;
}

内联后的代码:

public int sum(int a, int b, int c, int d){
    return a + b + c + d;
}

为什么方法内联能提升性能呢?

大家都知道函数调用其实就是对栈stack的操作,即压栈和出栈过程,当一个方法被调用,一个新的栈帧会被加到栈顶,分配的本地变量和参数会存储在这个栈帧,然后跳转到目标方法代码执行,方法返回的时候,本地方法和参数被销毁,栈顶被移除,最后返回到原来的地址执行。

所以函数调用需要有一定的时间和空间开销,当一个方法体不大,但又频繁被调用时,这个时间和空间开销会相对变得很大,这样就变得非常不划算,势必会降低程序的性能。根据二八原则,80%的性能消耗其实是发生在20%的代码上,对热点代码的针对性优化可以提升整体系统的性能。

但触发方法内联是有条件的,不是说加了final修饰就可以立即触发内联,还需要根据JVM的参数:-XX:CompileThreshold判断编译次数,-XX:MaxFreqInlineSize被内联方法体的大小限制。

所以说可以使用方法内联的业务场景是:

对于频繁调用的热点方法,并且方法体不大的,建议使用final修饰(private方法会隐式地被指定final修饰符,所以无须再为其指定final修饰符)。

二. public方法建议也尽量指定final修饰符

基于上面第一条,满足上述业务场景的方法,原因同上,利于JIT优化。当然也可以直接修饰方法所在的类上,这样final的类不允许被继承,而且该类的所有方法默认都是final的。

这里补充一条限制条件:并且没有被Spring AOP代理的方法

三. 能够使用lambda表达式的地方就不要用匿名内部类实现

lambda表达式不仅仅是语法糖,它编译后的class文件在jvm中执行的指令也是有区别的,使用的指令是invokeDynamic,相比于匿名内部类的调用上开销会更小一些,因为它没有匿名内部类的初始化过程,代码上也更简洁。

匿名内部类写法:

private static final ExecutorService thredPool = Executors.newCachedThreadPool();
thredPool.submit(new Runnable() { 
    @Override
    public void run() {
        // 业务逻辑
    }
});

lambda表达式写法:

private static final ExecutorService thredPool = Executors.newCachedThreadPool();
thredPool.submit(() -> {
    // 业务逻辑
});

四. 尽量不要在方法中频繁调用全局变量

在类中,成员变量(全局变量)保存在堆Heap中,函数内部的局部变量(基本类型、参数、对象的引用)都保存在栈Stack中,在函数内部访问自己的这些变量肯定要比访问函数外部的变量速度要快,所以从栈中操作堆中的数据速度会比较慢。

代码示例如下(反例):

private int result = 0; // 成员变量
public void sum(int... i){
    for (int j : i) {
        result += j; // 频繁操作函数外部的成员变量result
    }
}

修改后的代码:

private int result = 0; // 全局变量
public void sum(int... i){
    int temp = 0; // 临时变量
    for (int j : i) {
        temp += j; // 操作函数内部的临时变量
    }
    result = temp; // 减少对成员变量的访问
}

五. 优化集合操作中先contains再get的写法

我们的代码中经常会遇到要判断集合中是否存在这个元素,存在再取值的业务场景,伪代码如下:

public void setOrderPrice(Order order, Map<String, Price> map){
    if(map.containsKey(order.getId())){
        order.setPrice(map.get(order.getId()));
    } else {
        order.setPrice(new Price());
    }
}

其实可以直接调用get获取,然后判空,这样就省去了一次查找匹配的过程,修改后如下:

public void setOrderPrice(Order order, Map<String, Price> map){
    Price price = map.get(order.getId(); // 直接调用get
    if(price != null){
        order.setPrice(price);
    } else {
        order.setPrice(new Price());
    }
}

以上仅是作者这些年在Java开发性能方面的工作见解,性能优化需要在时间、效率、可读性各方面权衡,做出取舍,不要把上面的内容当成教条,活学活用,变通为宜。