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