【进阶之路】Java代码性能调优(二)

导言

大家好,我是南橘,从接触java到现在也有差不多两年时间了,两年时间,从一名连java有几种数据结构都不懂超级小白,到现在懂了一点点的进阶小白,学到了不少的东西。知识越分享越值钱,我这段时间总结(包括从别的大佬那边学习,引用)了一些平常学习和面试中的重点(自我认为),希望给大家带来一些帮助


有需要的同学可以加我的公众号,以后的最新的文章第一时间都在里面,也可以找我要思维导图

在这里插入图片描述

上一章介绍了字符串、数字和集合类的一些高效用法,这一章就继续查漏补缺、介绍更多的性能优化技巧。

【进阶之路】Java代码性能调优(一)

一、int转String的用法

从之前的文章可以得知,int到String的转换是一个耗时的操作,因为我们需要尽量避免做这些转换。如果实在需要,也可以动用上期Integer自动拆包装包的方法,预先将一部分的int值转化为字符串。

我们通过工具类,预先设定好1024个缓存(或者根据业务设置更多),所有调用int2String方法的时候都会预先判断数据是否在缓存内,如果小于1024,则会去调用缓存数据。

测试代码如下,我应该会在下下一章详细介绍我如何通过JMH来对代码性能进行测试
*

大家注意一下这三张图片Benchmark区域的信息不难发现,在数字1024以内,使用缓存的int2StringByCache的性能几乎高出int2String一个数量级。

这里就是运用了JDK对INTEGER自动拆箱装箱的原理

二、使用Native方法

Native方法就是调用一个非Java代码的接口。

我在之前的文章【进阶之路】攻克JVM——JVM的垃圾回收机制(二)里有讲过。

一般来说,作为java的底层代码,Native有着更好的性能。

最常用的Native方法是Stream.arraycopy方法,把原数组的内容复制到目标数组中:

 public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

里面还有各种各样的方法,大家可以根据情况来使用。

三、Switch优化

在条件判断中,如果有较多的分支判断,使用switch语句通常比使用if语句的效率更高。if语句会每次取出变量进行比较从而确定处理分支,而switch语句只需要取出一次变量,然后根据tableswitch直接找到分支即可。

根据测试,分支较少的情况下,if和switch的速度差不多,再分支较多的情况下,switch的速度就快多了。
这里大家可以自己测试一下。

我们知道JDK1.8之后,JDK支持String类型,是因为在变异的时候,使用hashCode来作为switch的实际值。
首先写一段switch结构的代码:

public static void main(String[] args) {
        final String str = "C";

        switch (str) {
            case "A" :
                System.out.println("A");
                break;
            case "B" :
                System.out.println("B");
                break;
            default:
                System.out.println("C");
        }
    }

public static void main(String[] args) {
        String str = "C";
        String var2 = "C";
        byte var3 = -1;
        switch(var2.hashCode()) {
        case 65:
            if (var2.equals("A")) {
                var3 = 0;
            }
            break;
        case 66:
            if (var2.equals("B")) {
                var3 = 1;
            }
        }

        switch(var3) {
        case 0:
            System.out.println("A");
            break;
        case 1:
            System.out.println("B");
            break;
        default:
            System.out.println("C");
        }

}

可以看到,switch结构中变为了String.hashcode()方法,利用其返回的int值进行判断,所以说编译后还是使用了switch(int)结构来实现的。而且我们知道,String的hashcode方法是有哈希冲突的风险的,所以我们应该在每个case条件中增加了equals作为补充判断,避免哈希冲突错误。

四、优先使用局部变量

当存取类变量的时候,Java使用的是虚拟机指令GETFFIELD获取类变量,如果存取方法的变量,则通过出栈操作获取变量。
在之前的文章【进阶之路】攻克JVM——JVM对象及对象的访问定位(一)里有提到

JVM提出栈上分配的概念,针对作用域在方法内的对象,如果满足了逃逸分析,就会将对象属性打散后分配在栈上(线程私有的,属于栈内存),这样,随着方法的调用结束,栈空间的回收就会随着将栈上分配的打散后的对象回收掉,不再给GC增加额外的无用负担,从而提升应用程序整体的性能。

因此在需要频繁操作类变量的时候,最好先赋值给一个局部变量,比如这样:

int[] arr = new int[1024];
        for (int i=0;i<1024;i++){
            arr[i]=i;
        }

但是在实际的开发过程中,由于CPU缓存的原因,并不是每次都从Heap(堆)中取出变量,会从CPU缓存中存取,所以在JMH测试中难以验证谁的性能更好,这边我就不展示了。但是,通过对JVM机制的学习,我们能清楚的知道局部变量的好处。

五、预处理、预分配、预编译

1、预处理

预处理指的是对于需要反复调用的代码,可以尝试取出公共的只读代码块,处理一次生成并保留处理结果。这样接下来需要反复调用的时候,可以直接引用处理的结果。

随便举个例子,假设是黑名单判断,没有做预处理的代码如下:

  ServiceConfig serviceConfig =new ServiceConfig("a,A,S,DA,ASD,F,SDF,SD,F,F,A,D,F");
        Set<String> strings = serviceConfig.getblackService();
         forbid =strings.contains("a");

然后稍微改一下,这里是做了预处理的

public class ServiceConfig{
    String black=null;
    Set<String> blackSet =new HashSet<>();
    public ServiceConfig(String black){
        this.black=black;
        this.blackSet.addAll(Arrays.asList(black.split(",")));
    }
    public Set<String> getblackService(){
        return blackSet;
    }
}

 ServiceConfig serviceConfig =new ServiceConfig("a,A,S,DA,ASD,F,SDF,SD,F,F,A,D,F");
        Set<String> strings = serviceConfig.getblackService();
         forbid =strings.contains("a");

然后放在测试环境一跑,大家看的很明显了,谁才是版本答案:

2、预分配

预分配就很简单了,JDK存在大量预先分配空间的代码,比如我上一章讲的StringBuilder,会初始分配一段空间,不必每次调用append时才分配。


每次在append之前,也会检查分配空间是否足够,如果足够,则不需要增加空间。

如果所有的业务代码都能预分配合理的空间,那么系统的业务性能也会有合理的提高。

3、预编译

JDBC在处理SQL语句时有一个预编译的过程,而预编译对象就是把一些格式固定的SQL编译后,存放在内存池中即JDBC缓冲池,当我们再次执行相同的SQL语句时就不需要预编译的过程了,所以即使SQL注入特殊的语句,也会只当做参数传进去,不会当做指令执行。这个功能一大优势就是能提高执行速度,尤其是多次操作数据库的情况,再一个优势就是预防SQL注入,严格的说,应该是预防绝大多数的SQL注入。

还有种用法,涉及到格式化、序列化的时候,预编译成长红箭格式是一种提高性能的办法,比如在日志输出的时候可能会采用这种方法,

String类的format()方法用于创建格式化的字符串以及连接多个字符串对象,会解析format中出现的“{}”符号

(类似这样)

预编译的方式也能提升服务的性能。

结语

我们在编写代码的过程中,稍稍一注意,就能全面提升代码的性能。这一次的系列文章也是出于这个角度所编写的,接下来我会继续的思考和查阅资料,进一步完善调优系列。

同时需要思维导图的话,可以联系我,毕竟知识越分享越香!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值