字节码指令其实是很重要的,在之前学习String等内容,深入到字节码层面很容易找到答案,而不是只是在网上寻找答案,还有可能是错误的。
PS:本文基于jdk1.8
首先写个简单的类:
public class Test { public static Integer f1() {
int a = 1;
int b = 2;
return a + b;
} public static void main(String[] args) {
int m = 100;
int c = f1();
System.out.println(m + c);
} }
反编译:
先通过javac编译,然后通过javap -verbose Test.class > Test.txt把反编译结果重定向到txt文件中
//类文件
Classfile /D:/Java/project/monitor/target/classes/com/it/test1/Test.class
//最后修改,文件大小
Last modified 2019-7-16; size 785 bytes
MD5 checksum 1dc6eb4c2e233f63edbb50e709c20111
//编译来自Test.java
Compiled from "Test.java"
//以下为类信息
public class com.it.test1.Test
//jdk版本
minor version: 0
major version: 52
//类的访问修饰符,public和super
flags: ACC_PUBLIC, ACC_SUPER
//2、常量池,下面1,2,3,4....50,相当于索引,这部分简单理解就行了,主要是程序部分
Constant pool:
//Methodref方法引用,#8.#29代表引用第8行和29行
#1 = Methodref #8.#29 // java/lang/Object."<init>":()V
//自动装箱,执行Integer.valueOf()
#2 = Methodref #30.#31 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#3 = Methodref #7.#32 // com/it/test1/Test.f1:()Ljava/lang/Integer;
#4 = Methodref #30.#33 // java/lang/Integer.intValue:()I
//Fieldref字段引用,L为引用类型,格式为L ClassName;注意最后还有冒号;
#5 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream;
#6 = Methodref #36.#37 // java/io/PrintStream.println:(I)V
//类
#7 = Class #38 // com/it/test1/Test
#8 = Class #39 // java/lang/Object
#Utf8可以理解为字符串,<init>相当于构造函数
#9 = Utf8 <init>
//()V,无参,返回值为void
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable //本地变量表
#14 = Utf8 this
#15 = Utf8 Lcom/it/test1/Test;
#16 = Utf8 f1
#17 = Utf8 ()Ljava/lang/Integer;
#18 = Utf8 a
#19 = Utf8 I
#20 = Utf8 b
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 args
#24 = Utf8 [Ljava/lang/String;
#25 = Utf8 m
#26 = Utf8 c
#27 = Utf8 SourceFile
#28 = Utf8 Test.java
//NameAndType,名称和返回值
#29 = NameAndType #9:#10 // "<init>":()V
#30 = Class #40 // java/lang/Integer
#31 = NameAndType #41:#42 // valueOf:(I)Ljava/lang/Integer;
#32 = NameAndType #16:#17 // f1:()Ljava/lang/Integer;
#33 = NameAndType #43:#44 // intValue:()I
#34 = Class #45 // java/lang/System
#35 = NameAndType #46:#47 // out:Ljava/io/PrintStream;
#36 = Class #48 // java/io/PrintStream
#37 = NameAndType #49:#50 // println:(I)V
#38 = Utf8 com/it/test1/Test
#39 = Utf8 java/lang/Object
#40 = Utf8 java/lang/Integer
#41 = Utf8 valueOf
#42 = Utf8 (I)Ljava/lang/Integer;
#43 = Utf8 intValue
#44 = Utf8 ()I
#45 = Utf8 java/lang/System
#46 = Utf8 out
#47 = Utf8 Ljava/io/PrintStream;
#48 = Utf8 java/io/PrintStream
#49 = Utf8 println
#50 = Utf8 (I)V
//程序部分开始
{
public com.it.test1.Test();
//默认构造器,无参,无返回值
descriptor: ()V
//修饰符public
flags: ACC_PUBLIC
//Code部分
Code:
# 操作数栈的深度2
# 本地变量表最大长度(slot为单位),64位的是2个,其他是1个,索引从0开始,如果是非static方法索引0代表this,后面是入参,后面是本地变量
# 1个参数,实例方法多一个this参数
//args_size
stack=1, locals=1, args_size=1
//aload_<n>从本地变量加载引用,n为当前栈帧中局部变量数组的索引
0: aload_0
//invokespecial调用实例方法; 对超类,私有和实例初始化方法调用的特殊处理
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
//行号的表,line后面的数字代表代码的行号,代表上面字节码中的0,就是aload_0
LineNumberTable:
line 3: 0
//本地变量表,非static方法,0位this,static方法,就是第一个变量
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/it/test1/Test; public static java.lang.Integer f1();
descriptor: ()Ljava/lang/Integer;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0
//将常量1push到操作数栈中
0: iconst_1
//将操作数栈中栈顶元素存储到本地变量表的索引0中
//这两步对应着int a = 1;
1: istore_0
2: iconst_2
//这两步对应着int b = 2;
3: istore_1
//将int类型的本地变量0的数据压入操作数栈
4: iload_0
5: iload_1
//int类型相加
6: iadd
//调用了第二行,是一个方法引用,执行完毕,清空操作数栈,此时本地变量表数据还在
7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//返回引用,会把本地变量表清空
10: areturn
LineNumberTable:
line 6: 0
line 7: 2
line 8: 4
LocalVariableTable:
Start Length Slot Name Signature
2 9 0 a I
4 7 1 b I public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: bipush 100
2: istore_1
//invokestatic执行静态方法,invokevirtual执行实例方法
3: invokestatic #3 // Method f1:()Ljava/lang/Integer;
6: invokevirtual #4 // Method java/lang/Integer.intValue:()I
9: istore_2
10: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_1
14: iload_2
15: iadd
16: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
19: return
LineNumberTable:
line 12: 0
line 13: 3
line 14: 10
line 15: 19
LocalVariableTable://数组类型参数,作为本地变量表索引0位置的数据
Start Length Slot Name Signature
0 20 0 args [Ljava/lang/String;
3 17 1 m I
10 10 2 c I
}
SourceFile: "Test.java"
上面对基本的字节码都有解释了,这里以f1()为例,通过图例更加详细的解释
字节码相关内容,可以参考官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/
1、flags表示类访问和属性修饰符
字段描述符:
方法描述符:
i++和++i
代码:
public static void f1() {
int i = 0;
int j = i++;
} public static void f2() {
int i = 0;
int j = ++i;
} public static void main(String[] args) {
f1();
f2();
}
反编译:
f1():
0: iconst_0 //常量0push到操作数栈的栈顶
1: istore_0 //将操作数栈的栈顶数据存储到本地变量表的索引0的位置
2: iload_0
3: iinc 0, 1 //将本地变量表的索引0的数据+1
6: istore_1 //将操作数栈的栈顶数据存储到本地变量表的索引1的位置
7: return f2():
0: iconst_0
1: istore_0
2: iinc 0, 1 //将本地变量表的索引0的数据+1
5: iload_0 //此时索引0的数据为1,load到操作数栈
6: istore_1 ////将操作数栈的栈顶数据存储到本地变量表的索引1的位置
7: return
从上面我们很容易看到二者的区别
PS:for循环中i++和++i没有效率差别,字节码完全一样的
try-Cache字节码:
代码:
public static String f1() {
String str = "hello1";
try{
return str;
}
finally{
str = "imooc";
}
} public static void main(String[] args) {
f1();
}
反编译:
0: ldc #2 //从运行时常量池中加载字符串hello,然后push到操作数栈
2: astore_0 //将操作数栈的栈顶数据存储到本地变量表的索引0的位置
3: aload_0 //
4: astore_1 //字符串hello,存在本地变量表的两个位置
5: ldc #3 // String imooc
7: astore_0 //将字符串imooc存储到本地变量表的索引0的位置,用imooc覆盖了hello
8: aload_1 //load本地变量表中索引1位置的数据
9: areturn //所以返回的是hello,而不是imooc //假如发生异常,就会走下面的代码
10: astore_2 //将异常存储到本地变量表的索引2的位置
11: ldc #3 // String imooc
13: astore_0
14: aload_2 //将索引2的位置的异常信息load出去
15: athrow //跑出异常
我们从字节码中看到无论是否发生异常,都会执行finally的内容,但是我们的return并不是finally的内容
我们再测试一下:
public static String f1() {
String str = "hello1";
try{
return str;
}
finally{
str = "imooc";
return "111";
}
} public static void main(String[] args) {
System.out.println(f1());
}
反编译:
0: ldc #2 // String hello1
2: astore_0
3: aload_0
4: astore_1
5: ldc #3 // String imooc
7: astore_0
8: ldc #4 // String 111 将字符串111push到操作数栈的栈顶
10: areturn 11: astore_2
12: ldc #3 // String imooc
14: astore_0
15: ldc #4 // String 111 将字符串111push到操作数栈的栈顶
17: areturn
所以无论是否发生异常,返回的都是字符串111,我们一般情况下不要在finally中使用return,很容易出现错误
String Constant Variable:
我们知道String的字符串拼接就是会new一个StringBuilder,然后append这个字符串,然后调用toString(),在for循环中效率很低。但是如果是final修饰的常量就不一定了。
代码:
public static void f1() {
final String x="hello";
final String y=x+"world";
String z=x+y;
System.out.println(z);
} public void f2(){
final String x="hello";
String y=x+"world";
String z=x+y;
System.out.println(z);
}
反编译:
f1():
0: ldc #2 // String hello //从常量池把hello字符串push到操作数栈
2: astore_0
3: ldc #3 // String helloworld //从常量池把helloworld字符串push到操作数栈
5: astore_1
6: ldc #4 // String hellohelloworld //从常量池把hellohelloworld字符串push到操作数栈
8: astore_2
9: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_2
13: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return f2():
0: ldc #2 // String hello
2: astore_1
3: ldc #3 // String helloworld
5: astore_2
6: new #7 // class java/lang/StringBuilder //new StringBuilder
9: dup //复制操作数栈的栈顶值
10: invokespecial #8 // Method java/lang/StringBuilder."<init>":()V //调用无参构造器初始化
13: ldc #2 // String hello
15: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_2
19: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_3
30: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
我们可以看到对于final修饰String引用,在编译器就进行了优化,x+"world"直接优化成"helloworld"