java虚拟机(十四)--字节码指令
字节码指令其实是很重要的,在之前学习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"