Java虚拟机实践(2)——常见代码的字节码分析实践

0. 说明

这里我们会提到的常见表达式有:

  • i++++i
  • 字符串拼接操作符 +
  • try-finally代码块
  • final String

我的测试环境为

  • 平台:Win10 Java 8
  • IDE: IntelliJ IDEA
  • 字节码查看器:Jclasslib ( 安装在IntelliJ IDEA里的插件)

1. i++与++i

首先我们编写一个最简单的程序来作为测试例程。

package jvm.bytecode;

public class IncByteCode {

    public static void addAndGet(){
        int i = 0;
        int j = i++;
        System.out.println(j);
    }

    public static void getAndAdd(){
        int i = 0;
        int j = ++i;
        System.out.println(j);
    }

    public static void main(String[] args) {
        addAndGet();
        getAndAdd();
    }
}

程序运行结果,在我们学习Java SE时应该就能够知道

0
1

现在我们分析一下两个函数addAndGet和getAndAdd的字节码
注意:这里复制过来的部分字节码语义是经过Jclasslib处理过的

addAndGet方法

 0 iconst_0 // 将0压入栈
 1 istore_0 // 将栈顶整数(也就是0)赋值给第一个局部变量(也就是i)
 2 iload_0 // 取第1个int局部变量压入栈顶
 3 iinc 0 by 1 // 第1个局部变量(即i)自增1
 6 istore_1 // 将栈顶元素(也就是之前压入栈顶的值为0的元素i)赋值给第2个局部变量(也就是j)
 7 getstatic #2 <java/lang/System.out> // 将静态变量System.out压入栈顶
10 iload_1 // 将第2个局部变量(也就是j)压入栈顶
11 invokevirtual #3 <java/io/PrintStream.println> // 调用println方法
14 return // 方法返回

getAndAdd方法

 0 iconst_0 // 将0压入栈
 1 istore_0 // 将栈顶整数(也就是0)赋值给第一个局部变量(也就是i)
 2 iinc 0 by 1 // 第一个局部变量(即i)自增1
 5 iload_0 // 将第1个局部变量(也就是i)压入栈顶
 6 istore_1 // 将栈顶元素(也就是i)赋值为第2个局部变量(也就是j)
 7 getstatic #2 <java/lang/System.out> // 将静态变量System.out压入栈顶
10 iload_1 // 将第2个局部变量(也就是j)压入栈顶
11 invokevirtual #3 <java/io/PrintStream.println> // 调用println方法
14 return // 方法返回

再看两段代码,我们现在要看看
for(int i = 0 ; i < 10 ; i++)for(int i = 0 ; i < 10 ; ++i)是否有性能影响

package jvm.bytecode;

public class IncByteCode2 {

    public static void addAndGet(){
        for(int i = 0 ; i < 10 ; i++){
            System.out.println(i);
        }
    }

    public static void getAndAdd(){
        for(int i = 0 ; i < 10 ; ++i){
            System.out.println(i);
        }
    }

    public static void main(String[] args) {
        addAndGet();
        getAndAdd();
    }
}

通过编译后,我们查看字节码,发现两个方法的字节码完全相同,所以它们之间没有性能差异

 0 iconst_0 // 将0推送至栈顶
 1 istore_0 // 将栈顶元素赋值给第1个局部变量(也就是i)
 2 iload_0 // 将第1个局部变量压入栈顶
 3 bipush 10 // 将10压入栈顶
 5 if_icmpge 21 (+16) // 如果i>=栈顶元素值(10),pc值+16也就是到21
 8 getstatic #2 <java/lang/System.out> // 取System.out置栈顶
11 iload_0 // 将第1个局部变量(也就是i)推送至栈顶
12 invokevirtual #3 <java/io/PrintStream.println> // 调用println
15 iinc 0 by 1 // 将第1个局部变量(也就是i)自增1
18 goto 2 (-16) // 跳转到pc值为当前pc-16,也就是2
21 return // 方法返回

2. 字符串拼接符 +

我们通过两段代码str = str + "X";stringBuilder.append("X");的字节码来分析字符串拼接与StringBuilder拼接效率

package jvm.bytecode;

public class StringAppend {

    public static void strConj(){
        String str = "";
        for(int i = 0 ; i < 10 ; i ++ ){
            str = str + "X";
        }
    }

    public static void strBderAppend(){
        StringBuilder sb = new StringBuilder();
        for(int i = 0 ; i < 10 ; i ++ ){
            sb.append("X");
        }
    }

    public static void main(String[] args) {
        strConj();
        strBderAppend();
    }
}

我们先看一看字符串拼接相关的方法字节码:

 0 ldc #2 // 推送空字符串到栈顶
 2 astore_0 // 将栈顶元素值(空字符串)赋值为第1个局部变量(应该是str)
 3 iconst_0 // 将0推送至栈顶
 4 istore_1 // 将栈顶元素(0)赋值给第2个局部变量(应该是i)
 5 iload_1 // 将第2个局部变量(i)推送至栈顶
 6 bipush 10 // 推送10至栈顶
 8 if_icmpge 37 (+29) // 如果i>=10那么,pc值就+29也就是跳到pc=37处
11 new #3 <java/lang/StringBuilder> // new StringBuilder()并将生成的实例推送至栈顶
14 dup // 拷贝栈顶元素并推送至栈顶
15 invokespecial #4 <java/lang/StringBuilder.<init>> // 调用StringBuilder的构造方法
18 aload_0 // 将第1个局部变量(str)推送至栈顶
19 invokevirtual #5 <java/lang/StringBuilder.append> // 调用append
22 ldc #6 <X> // 从常量池中找到"X"推送至栈顶
24 invokevirtual #5 <java/lang/StringBuilder.append> // 调用append
27 invokevirtual #7 <java/lang/StringBuilder.toString> // 调用SB的toString()方法
30 astore_0 // 将栈顶元素赋值给第1个局部变量(str)
31 iinc 1 by 1 // 将第2个局部变量自增1
34 goto 5 (-29) // pc值-29也就是转移到5
37 return // 返回

从字节码中我们知道这里的字符串拼接操作是通过StringBuilder来完成的:
str = str + "X";

从字节码的本质上看,可以分为如下4个步骤来执行:

StringBuilder sb = new StringBuilder();
sb.append("x");
sb.append(str);
str = sb.toString();

也就是说,我们每循环一次作一次拼接操作时,我们都会隐式地创建一个新的StringBuilder对象,并将所要拼接地字符串全部append进此StringBuilder中,最后调用StringBuilder的toString方法,将拼接结果赋值给str

由此,我们可以知道:字符串拼接操作符+效率是及其低下的。

所以我们可以知道,仅仅使用StringBuilder进行字符串拼接,它的效率肯定比单纯地用+来拼接字符串效率高得多。

下面我们分析strBderAppend方法地字节码来验证我们的想法。

 0 new #3 <java/lang/StringBuilder> // new StringBuilder()并将生成的实例推送至栈顶
 3 dup // 拷贝栈顶元素并推送至栈顶
 4 invokespecial #4 <java/lang/StringBuilder.<init>> // 调用StringBuilder的构造方法
 7 astore_0 // 将第1个局部变量(sb)推送至栈顶
 8 iconst_0 // 将0推送至栈顶
 9 istore_1 // 将栈顶元素(0)赋值给第2个局部变量(应该是i)
10 iload_1 // 将第2个局部变量(i)推送至栈顶
11 bipush 10 // 推送10至栈顶
13 if_icmpge 29 (+16) // 如果i>=10那么,pc值就+16也就是跳到pc=29处
16 aload_0 // 将第1个局部变量(sb)推送至栈顶
17 ldc #6 <X> // 从常量池中找到"X"推送至栈顶
19 invokevirtual #5 <java/lang/StringBuilder.append> // 调用append
22 pop // 弹出栈顶元素sb
23 iinc 1 by 1 // 将第2个局部变量自增1
26 goto 10 (-16) // pc值-16也就是转移到10
29 return // 方法返回

从代码中,我们知道,new StringBuilder()只发生一次,每次连接字符串使用的都是append方法,所以效率比加号+连接字符串效率高得多

3. try-finally 代码块

首先看一段代码,先猜测一下它输出什么

public class TryFinallyByteCode {

    public static String tryfinally(){
        String str = "hello";
        try{
            return str;
        } finally {
            str = "world";
        }
    }

    public static void main(String[] args) {
        System.out.println(tryfinally());
    }
}

答案是输出"hello"

hello

话不多说,直接分析字节码吧:

 0 ldc #2 <hello> // 从常量池找到"hello",将其压入栈顶
 2 astore_0 // 将栈顶元素("hello")赋值给第1个局部变量(str)
 3 aload_0 // 将第1个局部变量(str)压入栈顶
 4 astore_1 // 将栈顶元素(str="hello")值赋值给第2个局部变量
 5 ldc #3 <world> // 从常量池找到"world",将其压入栈顶
 7 astore_0 // 将栈顶元素("world")值赋值给第1个局部变量(str)
 8 aload_1 // 将第2个局部变量(值为"hello")压入栈顶
 9 areturn // 从方法返回引用(返回的是"hello")
// 发生异常时会运行下面的代码
10 astore_2 // 将栈顶元素赋值给第3个局部变量
11 ldc #3 <world> //从常量池找到"world",将其压入栈顶
13 astore_0 // 将栈顶元素赋值给第1个局部变量
14 aload_2 // 将第2个局部变量值压入栈顶
15 athrow // 抛出异常

4. final String

我们先看一下下面的一段代码,我们分析一下有final修饰的String,与没有final修饰的String,在字节码上有什么不同

public class FinalStringTest {

    public static void notHavaFinal(){
        final String str1 = "hello";
        String str2 = str1 + "world";
        String str3 = str1 + str2;
    }

    public static void haveFinal(){
        final String str1 = "hello";
        final String str2 = str1 + "world";
        String str3 = str1 + str2;
    }

    public static void main(String[] args) {
        notHavaFinal();
        haveFinal();
    }
}

我们分析两个函数notHavaFinal()haveFinal()的字节码:
首先看没有final修饰的函数notHavaFinal()

 0 ldc #2 <hello> 
 2 astore_0
 3 ldc #3 <helloworld>
 5 astore_1
 6 new #4 <java/lang/StringBuilder>
 9 dup
10 invokespecial #5 <java/lang/StringBuilder.<init>>
13 ldc #2 <hello>
15 invokevirtual #6 <java/lang/StringBuilder.append>
18 aload_1
19 invokevirtual #6 <java/lang/StringBuilder.append>
22 invokevirtual #7 <java/lang/StringBuilder.toString>
25 astore_2
26 return

根据分析,我们知道str2并非通过StringBuilder构造出来的,我们现在猜测很可能是由str1的final关键字影响的。(后面会给出原因),对于str3,str3与我们上节分析的结果是一样的:通过StringBuilder构造出来。

我们现在分析有final修饰str的函数havaFinal()

0 ldc #2 <hello>
2 astore_0
3 ldc #3 <helloworld>
5 astore_1
6 ldc #8 <hellohelloworld>
8 astore_2
9 return

我们看到,havaFinal()方法的字节码非常简短,但是却能够反映出很多问题:str1,str2,str3都是直接进行赋值的,与StringBuilder没有任何关系

实际上,这种final修饰的场景,我们称之为编译时替换。跟C/C++的#define类似。
Java中,final修饰的String,编译时会将它放在常量池中。

现在我们再来看看字面常量(String Literals),它也是放在常量池中。先看看下面的代码:

package testPackage;
class Test {
    public static void main(String[] args) {
        String hello = "Hello", lo = "lo";
        // 1. 相同类,相同包
        System.out.print((hello == "Hello") + " ");// Literal strings
        // 2. 不同类,相同包
        System.out.print((Other.hello == hello) + " ");// Literal strings
        // 3. 不同包,不同类
        System.out.print((other.Other.hello == hello) + " ");// Literal strings
        // 4. 常量表达式,编译时会放入作为常量池成员
        System.out.print((hello == ("Hel"+"lo")) + " ");// constant expressions 
        // 5. 运行时拼接, 相当于StringBuilder.toString();
        System.out.print((hello == ("Hel"+lo)) + " "); // "Hel"+lo是运行时创建
        // 6. intern的作用,查看下面列出的文章链接"深入解析String#intern"
        System.out.println(hello == ("Hel"+lo).intern());// intern 
    }
}
class Other { static String hello = "Hello"; }
package other;
public class Other { public static String hello = "Hello"; }

输出结果:

true true true true false true

https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4
https://docs.oracle.com/javase/specs/jls/se8/html/jls-3.html#jls-3.10.5
深入解析String#intern

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值