从java字节码指令看代码优化

一、前言

本篇文章我们谈一下如何从字节码指令方面做代码优化的分析,不懂字节码指令的朋友也没关系,我会解释指令的执行过程,重点是让我们了解到优化的原理,提升我们以后写代码的功底

二、优化场景1

1、我们首先看一段简单的代码,你能分析出哪个方法更优吗

    public void test1() {
        int i = 0;
        i += 1;
    }

    public void test2() {
        long i = 0;
        i += 1;
    }

2、然后我们再看方法对应的字节码指令,进行分析

test1

0 iconst_0
1 istore_1
2 iinc 1 by 1
5 return

加载常量0到本地方法栈,出栈0放入局部变量表索引1的位置,将局部变量表索引1位置的值自增1,返回

test2

0 lconst_0
1 lstore_1
2 lload_1
3 lconst_1
4 ladd
5 lstore_1
6 return

加载常量0到本地方法栈,出栈0放入局部变量表索引1的位置,加载局部变量表索引1位置的值到本地方法栈,加载常量1到本地方法栈,出栈本地方法栈的两个值相加后再把结果入栈,出栈结果放入局部变量表索引1的位置,返回

3、得出结论

从两个指令的分析中可以看出,方法1的代码明显更优,所以实际开发中我们可以多使用int型变量,可以在一定程度上达到优化的效果

三、优化场景2

1、我们同样先来看一段简单的代码,说出你觉得更优的方法

    public int test3(int i) {
        return i * 3;
    }

    public int test4(int i) {
        int j = i * 3;
        return j;
    }

2、我们看两个方法的指令,进行分析

test3

0 iload_1
1 iconst_3
2 imul
3 ireturn

加载局部变量表索引1位置的值到本地方法栈,加载常量3到本地方法栈,出栈两个值相乘结果入栈,最后把栈中结果返回

test4

0 iload_1
1 iconst_3
2 imul
3 istore_2
4 iload_2
5 ireturn

加载局部变量表索引1位置的值到本地方法栈,加载常量3到本地方法栈,出栈两个值相乘结果入栈,把栈中结果放入局部变量表索引2的位置,加载局部变量表索引2位置的值到本地方法栈,把栈顶元素返回

3、得出结论

根据指令的分析我们同样能很轻松的看出方法3更优,所以开发过程中我们可以尽量直接返回运算的结果,不用先声明变量接收再返回

四、优化场景3

1、我们先看一段简单的代码,你可以说出两个方法的结果吗

    public void test5() {
        long i = 123123123;
        float j = i;
        System.out.println(j);
    }
    
    public void test6() {
        long i = 123123123;
        BigDecimal b = new BigDecimal(i);
        System.out.println(b);
    }

2、我们看两个方法的指令,进行分析

test5

 0 ldc2_w #2 <123123123>
 3 lstore_1
 4 lload_1
 5 l2f
 6 fstore_3
 7 getstatic #4 <java/lang/System.out>
10 fload_3
11 invokevirtual #5 <java/io/PrintStream.println>
14 return

加载常量池中的值到本地方法栈,出栈元素放入局部变量表索引1的位置,加载局部变量表索引1位置的值到栈中,出栈元素进行宽化类型转换,转为float型变量,再把结果入栈,出栈元素放入局部变量表索引3的位置,获取静态字段放入栈中,加载局部变量表索引3位置的值到本地方法栈,出栈两个元素调用打印方法输出结果,返回

test6

0 ldc2_w #2 <123123123>
 3 lstore_1
 4 new #6 <java/math/BigDecimal>
 7 dup
 8 lload_1
 9 invokespecial #7 <java/math/BigDecimal.<init>>
12 astore_3
13 getstatic #4 <java/lang/System.out>
16 aload_3
17 invokevirtual #8 <java/io/PrintStream.println>
20 return

这里没有进行类型转换的操作,所以不存在类型转换时精度损失的问题

3、得出结论

这里我们主要说明的是类型转换时出现精度损失的问题,使用BigDecimal对象就可以避免这种情况的发生,所以实际开发中我们最好使用该对象

五、优化场景4

1、我们先看一段简单的代码,说出你认为更优的方法

    public void test7(int i) {
        switch (i) {
            case 1:
                break;
            case 2:
                break;
            case 3:
                break;
            default:
        }
    }

    public void test8(int i) {
        switch (i) {
            case 1:
                break;
            case 3:
                break;
            case 15:
                break;
            default:
        }
    }

2、我们看两方法的指令,进行分析

test7

 0 iload_1
 1 tableswitch 1 to 3	1:  28 (+27)
	2:  31 (+30)
	3:  34 (+33)
	default:  37 (+36)
28 goto 37 (+9)
31 goto 37 (+6)
34 goto 37 (+3)
37 return

加载局部变量表索引1位置的值到本地方法栈,出栈元素到跳转表进行判断,根据值跳转到对应指令的执行位置,继续往后执行

test8

 0 iload_1
 1 lookupswitch 3
	1:  36 (+35)
	3:  39 (+38)
	15:  42 (+41)
	default:  45 (+44)
36 goto 45 (+9)
39 goto 45 (+6)
42 goto 45 (+3)
45 return

加载局部变量表索引1位置的值到本地方法栈,出栈元素到查找表进行判断,在查找表中依次往下判断是否满足条件,直到满足条件或遍历完整个表时才结束

3、得出结论
从分析中可以看出,跳转表的效率明显高于查找表,可以直接跳转到对应指令的执行位置,而不用遍历整个表,所以方法7优于方法8

六、优化场景5

1、我们先看一段简单的代码,分析出你认为更优的方法

public class Code2 {
    private static Object obj;

    public static void test9() {
        obj = new Object();
    }

    public static void test10() {
        Object obj2 = new Object();
    }

    public static void main(String[] args) {
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            test10();
        }
        long t2 = System.currentTimeMillis();
        System.out.println(t2 - t1);
    }
}

2、代码分析
这里主要用到了JDK 6u23后开启的一种新技术,逃逸分析、标量替换和栈上分配,方法9由于创建的对象被类的字段所引用,所以虚拟机就认为该对象发生了逃逸,在堆中创建对象,而方法10创建的是局部对象未发生逃逸,所以创建的对象直接存储在栈中,极大节省了堆内存,测试效率更高

3、得出结论
通过分析得知,使用栈上分配的结果更好,所以实际开发中我们可以运用上这种技术,能使用局部变量的,就不要在方法外定义

总结

这些只是我个人总结的一些优化小技巧,如果有错误的话各位朋友可以指正,也希望在以后的学习过程中,我们能学到更多的优化方法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值