一、前言
本篇文章我们谈一下如何从字节码指令方面做代码优化的分析,不懂字节码指令的朋友也没关系,我会解释指令的执行过程,重点是让我们了解到优化的原理,提升我们以后写代码的功底
二、优化场景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、得出结论
通过分析得知,使用栈上分配的结果更好,所以实际开发中我们可以运用上这种技术,能使用局部变量的,就不要在方法外定义
总结
这些只是我个人总结的一些优化小技巧,如果有错误的话各位朋友可以指正,也希望在以后的学习过程中,我们能学到更多的优化方法