之前分析的MyTest1程序是比较简单的程序,接来下我们将再用一个程序来巩固一下对JVM字节码的理解:
package com.leolin.jvm.bytecode; public class MyTest2 { String str = "Welcome"; private int x = 5; public static Integer in = 10; private Object obj = new Object(); public MyTest2() { } public MyTest2(int a) { } public static void main(String[] args) { MyTest2 myTest2 = new MyTest2(); myTest2.setX(8); in = 20; } private synchronized void setX(int x) { this.x = x; } private void test(String str) { synchronized (obj) { System.out.println("str:" + str); } } static { System.out.println("hello"); } }
上面的代码如果从Java语言层面来看,是比较简单的,但是经过编译之后,对应的字节码文件,就不一定了。这里我们编译后再用javap -verbose -p来看看反编译之后的方法,这里之所比相比以前加上一个-p,是因为如果单单使用-verbose不会打印我们另外两个私有方法。反编译结果如下:
D:Fjava_spacejvm-lecture argetclasses>javap -verbose -p com.leolin.jvm.bytecode.MyTest2 Classfile /D:/F/java_space/jvm-lecture/target/classes/com/leolin/jvm/bytecode/MyTest2.class Last modified 2020-5-14; size 1578 bytes MD5 checksum e247d6b8aea41e469b1ebcd282f8eabb Compiled from "MyTest2.java" public class com.leolin.jvm.bytecode.MyTest2 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #5.#53 // java/lang/Object."<init>":()V #2 = String #54 // Welcome #3 = Fieldref #7.#55 // com/leolin/jvm/bytecode/MyTest2.str:Ljava/lang/String; #4 = Fieldref #7.#56 // com/leolin/jvm/bytecode/MyTest2.x:I #5 = Class #57 // java/lang/Object #6 = Fieldref #7.#58 // com/leolin/jvm/bytecode/MyTest2.obj:Ljava/lang/Object; #7 = Class #59 // com/leolin/jvm/bytecode/MyTest2 #8 = Methodref #7.#53 // com/leolin/jvm/bytecode/MyTest2."<init>":()V #9 = Methodref #7.#60 // com/leolin/jvm/bytecode/MyTest2.setX:(I)V #10 = Methodref #61.#62 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer; #11 = Fieldref #7.#63 // com/leolin/jvm/bytecode/MyTest2.in:Ljava/lang/Integer; #12 = Fieldref #64.#65 // java/lang/System.out:Ljava/io/PrintStream; #13 = Class #66 // java/lang/StringBuilder #14 = Methodref #13.#53 // java/lang/StringBuilder."<init>":()V #15 = String #67 // str: #16 = Methodref #13.#68 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #17 = Methodref #13.#69 // java/lang/StringBuilder.toString:()Ljava/lang/String; #18 = Methodref #70.#71 // java/io/PrintStream.println:(Ljava/lang/String;)V #19 = String #72 // hello #20 = Utf8 str #21 = Utf8 Ljava/lang/String; #22 = Utf8 x #23 = Utf8 I #24 = Utf8 in #25 = Utf8 Ljava/lang/Integer; #26 = Utf8 obj #27 = Utf8 Ljava/lang/Object; #28 = Utf8 <init> #29 = Utf8 ()V #30 = Utf8 Code #31 = Utf8 LineNumberTable #32 = Utf8 LocalVariableTable #33 = Utf8 this #34 = Utf8 Lcom/leolin/jvm/bytecode/MyTest2; #35 = Utf8 (I)V #36 = Utf8 a #37 = Utf8 main #38 = Utf8 ([Ljava/lang/String;)V #39 = Utf8 args #40 = Utf8 [Ljava/lang/String; #41 = Utf8 myTest2 #42 = Utf8 setX #43 = Utf8 test #44 = Utf8 (Ljava/lang/String;)V #45 = Utf8 StackMapTable #46 = Class #59 // com/leolin/jvm/bytecode/MyTest2 #47 = Class #73 // java/lang/String #48 = Class #57 // java/lang/Object #49 = Class #74 // java/lang/Throwable #50 = Utf8 <clinit> #51 = Utf8 SourceFile #52 = Utf8 MyTest2.java #53 = NameAndType #28:#29 // "<init>":()V #54 = Utf8 Welcome #55 = NameAndType #20:#21 // str:Ljava/lang/String; #56 = NameAndType #22:#23 // x:I #57 = Utf8 java/lang/Object #58 = NameAndType #26:#27 // obj:Ljava/lang/Object; #59 = Utf8 com/leolin/jvm/bytecode/MyTest2 #60 = NameAndType #42:#35 // setX:(I)V #61 = Class #75 // java/lang/Integer #62 = NameAndType #76:#77 // valueOf:(I)Ljava/lang/Integer; #63 = NameAndType #24:#25 // in:Ljava/lang/Integer; #64 = Class #78 // java/lang/System #65 = NameAndType #79:#80 // out:Ljava/io/PrintStream; #66 = Utf8 java/lang/StringBuilder #67 = Utf8 str: #68 = NameAndType #81:#82 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #69 = NameAndType #83:#84 // toString:()Ljava/lang/String; #70 = Class #85 // java/io/PrintStream #71 = NameAndType #86:#44 // println:(Ljava/lang/String;)V #72 = Utf8 hello #73 = Utf8 java/lang/String #74 = Utf8 java/lang/Throwable #75 = Utf8 java/lang/Integer #76 = Utf8 valueOf #77 = Utf8 (I)Ljava/lang/Integer; #78 = Utf8 java/lang/System #79 = Utf8 out #80 = Utf8 Ljava/io/PrintStream; #81 = Utf8 append #82 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #83 = Utf8 toString #84 = Utf8 ()Ljava/lang/String; #85 = Utf8 java/io/PrintStream #86 = Utf8 println { java.lang.String str; descriptor: Ljava/lang/String; flags: private int x; descriptor: I flags: ACC_PRIVATE public static java.lang.Integer in; descriptor: Ljava/lang/Integer; flags: ACC_PUBLIC, ACC_STATIC private java.lang.Object obj; descriptor: Ljava/lang/Object; flags: ACC_PRIVATE public com.leolin.jvm.bytecode.MyTest2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String Welcome 7: putfield #3 // Field str:Ljava/lang/String; 10: aload_0 11: iconst_5 12: putfield #4 // Field x:I 15: aload_0 16: new #5 // class java/lang/Object 19: dup 20: invokespecial #1 // Method java/lang/Object."<init>":()V 23: putfield #6 // Field obj:Ljava/lang/Object; 26: return LineNumberTable: line 9: 0 line 4: 4 line 5: 10 line 7: 15 line 10: 26 LocalVariableTable: Start Length Slot Name Signature 0 27 0 this Lcom/leolin/jvm/bytecode/MyTest2; public com.leolin.jvm.bytecode.MyTest2(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String Welcome 7: putfield #3 // Field str:Ljava/lang/String; 10: aload_0 11: iconst_5 12: putfield #4 // Field x:I 15: aload_0 16: new #5 // class java/lang/Object 19: dup 20: invokespecial #1 // Method java/lang/Object."<init>":()V 23: putfield #6 // Field obj:Ljava/lang/Object; 26: return LineNumberTable: line 12: 0 line 4: 4 line 5: 10 line 7: 15 line 13: 26 LocalVariableTable: Start Length Slot Name Signature 0 27 0 this Lcom/leolin/jvm/bytecode/MyTest2; 0 27 1 a I public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #7 // class com/leolin/jvm/bytecode/MyTest2 3: dup 4: invokespecial #8 // Method "<init>":()V 7: astore_1 8: aload_1 9: bipush 8 11: invokespecial #9 // Method setX:(I)V 14: bipush 20 16: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 19: putstatic #11 // Field in:Ljava/lang/Integer; 22: return LineNumberTable: line 17: 0 line 18: 8 line 19: 14 line 20: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 args [Ljava/lang/String; 8 15 1 myTest2 Lcom/leolin/jvm/bytecode/MyTest2; private synchronized void setX(int); descriptor: (I)V flags: ACC_PRIVATE, ACC_SYNCHRONIZED Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #4 // Field x:I 5: return LineNumberTable: line 23: 0 line 24: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/leolin/jvm/bytecode/MyTest2; 0 6 1 x I private void test(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PRIVATE Code: stack=3, locals=4, args_size=2 0: aload_0 1: getfield #6 // Field obj:Ljava/lang/Object; 4: dup 5: astore_2 6: monitorenter 7: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream; 10: new #13 // class java/lang/StringBuilder 13: dup 14: invokespecial #14 // Method java/lang/StringBuilder."<init>":()V 17: ldc #15 // String str: 19: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: aload_1 23: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 26: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 29: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 32: aload_2 33: monitorexit 34: goto 42 37: astore_3 38: aload_2 39: monitorexit 40: aload_3 41: athrow 42: return Exception table: from to target type 7 34 37 any 37 40 37 any LineNumberTable: line 27: 0 line 28: 7 line 29: 32 line 30: 42 LocalVariableTable: Start Length Slot Name Signature 0 43 0 this Lcom/leolin/jvm/bytecode/MyTest2; 0 43 1 str Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 255 /* full_frame */ offset_delta = 37 locals = [ class com/leolin/jvm/bytecode/MyTest2, class java/lang/String, class java/lang/Object ] stack = [ class java/lang/Throwable ] frame_type = 250 /* chop */ offset_delta = 4 static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: bipush 10 2: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: putstatic #11 // Field in:Ljava/lang/Integer; 8: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #19 // String hello 13: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: return LineNumberTable: line 6: 0 line 33: 8 line 34: 16 } SourceFile: "MyTest2.java"
首先,我们来分析两个构造方法:
public com.leolin.jvm.bytecode.MyTest2(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String Welcome 7: putfield #3 // Field str:Ljava/lang/String; 10: aload_0 11: iconst_5 12: putfield #4 // Field x:I 15: aload_0 16: new #5 // class java/lang/Object 19: dup 20: invokespecial #1 // Method java/lang/Object."<init>":()V 23: putfield #6 // Field obj:Ljava/lang/Object; 26: return LineNumberTable: line 9: 0 line 4: 4 line 5: 10 line 7: 15 line 10: 26 LocalVariableTable: Start Length Slot Name Signature 0 27 0 this Lcom/leolin/jvm/bytecode/MyTest2; public com.leolin.jvm.bytecode.MyTest2(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String Welcome 7: putfield #3 // Field str:Ljava/lang/String; 10: aload_0 11: iconst_5 12: putfield #4 // Field x:I 15: aload_0 16: new #5 // class java/lang/Object 19: dup 20: invokespecial #1 // Method java/lang/Object."<init>":()V 23: putfield #6 // Field obj:Ljava/lang/Object; 26: return LineNumberTable: line 12: 0 line 4: 4 line 5: 10 line 7: 15 line 13: 26 LocalVariableTable: Start Length Slot Name Signature 0 27 0 this Lcom/leolin/jvm/bytecode/MyTest2; 0 27 1 a I
第一个是无参构造方法,但很奇怪的是,第一个构造方法的参数数量args_size为1。这是因为Java在调用实例方法时,每个方法的第一个参数都是接收一个this,而Java会隐式的帮我们把当前对象传入实例方法。比如下面的A类:
class A { public void test() { } }
当我们调用一个A的实例方法test时,会转换成这样的形式:A.test(this),同理构造方法。这样我们也就能够理解,为什么MyTest2第二个构造方法的的参数数量args_size为2。第一个是Java帮我们隐式传入的this,第二个是我们显式声明的int类型变量a。我们知道,如果我们在类里为成员变量赋值,这些赋值的动作其实是在构造方法里完成。这里我们可以看到,两个构造方法中对应的字节码指令其实是一样的,也就是说,给成员变量赋值的字节码指令,会在每一个构造方法中冗余一份。我们来看下构造方法中的字节码指令:
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String Welcome 7: putfield #3 // Field str:Ljava/lang/String; 10: aload_0 11: iconst_5 12: putfield #4 // Field x:I 15: aload_0 16: new #5 // class java/lang/Object 19: dup 20: invokespecial #1 // Method java/lang/Object."<init>":()V 23: putfield #6 // Field obj:Ljava/lang/Object; 26: return
在构造方法中,先执行aload_0指令加载this的引用到栈顶,再执行invokespecial指令调用this的父类Object的构造方法,完成父类的初始化。执行完偏移0和1的指令之后,this引用会从栈中弹出,所以在此执行aload_0加载this到栈顶,然后执行ldc指令,加载常量池的元素第二个元素:Welcome到栈顶。putfield指令接收一个参数,即要在对象中设置值的字段,然后从栈中弹出两个元素,即是this引用和Welcome,完成成员变量str的赋值,这是偏移4到7所做的事。接着在偏移10到12完成字段x的赋值。在偏移15到23之间,依旧是加载this到栈顶,用new指令创建一个Object的对象然后推至栈顶,接着调用dup指令,根据栈顶的Object对象引用再次创建一份引用推至栈顶,至此栈顶有连续两个同一Object对象的引用,执行invokespecial指令,会调用Object对象的构造方法,然后弹出Object对象的引用,此时栈顶还有一个Object对象和this对象的引用,执行putfield指令,将栈中的两个引用弹出,把Object对象的引用设置进this对象的obj字段中。最后,执行偏移26的指令return,构造方法结束。
接着,我们分析main方法:
public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #7 // class com/leolin/jvm/bytecode/MyTest2 3: dup 4: invokespecial #8 // Method "<init>":()V 7: astore_1 8: aload_1 9: bipush 8 11: invokespecial #9 // Method setX:(I)V 14: bipush 20 16: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 19: putstatic #11 // Field in:Ljava/lang/Integer; 22: return LineNumberTable: line 17: 0 line 18: 8 line 19: 14 line 20: 22 LocalVariableTable: Start Length Slot Name Signature 0 23 0 args [Ljava/lang/String; 8 15 1 myTest2 Lcom/leolin/jvm/bytecode/MyTest2;
因为main方法是静态方法,静态方法就不像之前的实例方法,会隐式传入一个this,所以main方法设定传入一个数组参数,args_size即为1。首先,偏移0~7完成将创建的MyTest2实例压入栈顶,复制一份实例的引用压入栈顶,然后弹出引用执行构造方法,之后执行astore_1弹出MyTest2实例,将其保存在局部变量表索引为1的位置,,至此,操作数栈中不存在任何元素。
然后在偏移8到11之间,先执行aload_1,将局部变量表索引为1的引用加载到栈顶,这里是之前创建的MyTest2实例,再执行bipush,将一个字节长度的数字推送到栈顶,这里是8。之后调用invokespecial,从栈中弹出两个元素,将8作为参数传入MyTest2实例的方法setX(int x)。
最后在偏移14到22之间,执行bipush,将20推送至栈顶,然后调用invokestatic指令,执行Integer类中的静态方法valueOf(String s),将原先栈顶的元素20弹出传入该方法中,得到一个Integer对象后压入栈顶。执行putstatic,从栈顶弹出Integer对象,这只为类中静态变量in的值。
接下来,我们来看我们定义的方法setX(int x)和void text(),这两个都是同步方法:
…… private synchronized void setX(int); descriptor: (I)V flags: ACC_PRIVATE, ACC_SYNCHRONIZED Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #4 // Field x:I 5: return …… private void test(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PRIVATE Code: stack=3, locals=4, args_size=2 0: aload_0 1: getfield #6 // Field obj:Ljava/lang/Object; 4: dup 5: astore_2 6: monitorenter 7: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream; 10: new #13 // class java/lang/StringBuilder 13: dup 14: invokespecial #14 // Method java/lang/StringBuilder."<init>":()V 17: ldc #15 // String str: 19: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: aload_1 23: invokevirtual #16 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 26: invokevirtual #17 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 29: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 32: aload_2 33: monitorexit 34: goto 42 37: astore_3 38: aload_2 39: monitorexit 40: aload_3 41: athrow 42: return ……
首先是setX(int x),我们在方法的声明处加上synchronized,因此方法的访问标志就有ACC_SYNCHRONIZED。当有多个线程调用setX(int x)方法时,该方法是通过对MyTest2的class对象加锁和释放锁来实现的同步。
而在test()方法中,我们是通过synchronized块对实例内的对象obj上锁来实现同步的,这里有两个我们之前没见过的命令monitorenter和monitorexit。每个对象都会关联一个监视器,监视器有个条目计数,执行monitorenter指令时进入对象的监视器,如果监视器的条目计数为0,则设置为1,当前线程为监视器的所有者。线程还可以进入其他的synchronized块,如果synchronized块上锁的对象所关联的监视器归当前线程所拥有,会对监视器的条目计数加1。其他线程如果想拥有监视器的所有权,必须等到监视器条目为0的时候,才可以获取监视器的所有权。
那我们很容易想到monitorexit的作用,当线程离开一个synchronized块时,synchronized块所关联的监视器的条目计数就会减1。当监视器的条目计数减为0时,代表监视器此时无无主的状态,其他因为监视器而陷入阻塞的线程就能获得该监视器的所有权。我们注意到,上面的monitorenter指令只有一条,而monitorexit却不止一条。这是因为synchronized块的入口只有一个,出口却可以有好几个,如果代码没有异常的话,synchronized块应该是从上执行到下,先执行monitorenter对监视器的条目计数加1,最后执行monitorexit对监视器条目减1,这是最理想的状态。但是我们的程序是有可能抛出异常的,当异常抛出时,我们也要对监视器的条目计数减1,这就要求Java要能观察出程序中所有有可能抛出异常的地方,当异常发生要对监视器的条目计数减1,否则其他需要监视器的线程将永远陷入阻塞的状态。
最后是我们的静态代码块,在一个类中,会在一个名为clinit的静态方法中去执行所有静态变量的赋值、以及静态代码块的执行。根据之前类加载机制,我们知道类的加载和链接,只是完成静态变量内存的分配,以及赋予静态变量默认值,比如int的默认值为0,对象的默认值为null。但是在初始化的时候,才会将程序员赋予变量的值,赋予静态变量,即在clinit中。
…… static {}; descriptor: ()V flags: ACC_STATIC Code: stack=2, locals=0, args_size=0 0: bipush 10 2: invokestatic #10 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 5: putstatic #11 // Field in:Ljava/lang/Integer; 8: getstatic #12 // Field java/lang/System.out:Ljava/io/PrintStream; 11: ldc #19 // String hello 13: invokevirtual #18 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 16: return LineNumberTable: line 6: 0 line 33: 8 line 34: 16 ……
在MyTest2这个类中,我们声明了一个Integer类型的静态变量in,而且还有一个静态代码块,静态代码块中我们打印了一个hello。可以看到,上面偏移0到5和偏移8到13分别对应静态变量in赋值为10和获取IO流打印hello。至此,MyTest2的方法分析完毕。