java(1.8)字节码读例(匿名内部类)

Vanna ·
更新时间:2024-09-20
· 767 次阅读

源代码 public class ThreadDemo { public static void main(String[] args) { for(int i = 0;i<10;i++){ final int j = i; new Thread(new Runnable() { @Override public void run() { System.out.println("线程" + j); } }) .start(); } } }

javac -g ThreadDemo.java命令编译后生成两个class文件:ThreadDemo.classThreadDemo$1.class

字节码

利用jdk提供的反编译工具:javap -v class文件
得到如下两段字节码:

1.ThreadDemo.class Classfile /D:/work/workspace/util-demo/src/main/java/com/gsy/utildemo/thread/api/ThreadDemo.class Last modified 2020-3-20; size 716 bytes MD5 checksum 5837aed4e038d345b4e4089e35479b94 Compiled from "ThreadDemo.java" public class com.gsy.utildemo.thread.api.ThreadDemo minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #8.#27 // java/lang/Object."":()V #2 = Class #28 // java/lang/Thread #3 = Class #29 // com/gsy/utildemo/thread/api/ThreadDemo$1 #4 = Methodref #3.#30 // com/gsy/utildemo/thread/api/ThreadDemo$1."":(I)V #5 = Methodref #2.#31 // java/lang/Thread."":(Ljava/lang/Runnable;)V #6 = Methodref #2.#32 // java/lang/Thread.start:()V #7 = Class #33 // com/gsy/utildemo/thread/api/ThreadDemo #8 = Class #34 // java/lang/Object #9 = Utf8 InnerClasses #10 = Utf8 #11 = Utf8 ()V #12 = Utf8 Code #13 = Utf8 LineNumberTable #14 = Utf8 LocalVariableTable #15 = Utf8 this #16 = Utf8 Lcom/gsy/utildemo/thread/api/ThreadDemo; #17 = Utf8 main #18 = Utf8 ([Ljava/lang/String;)V #19 = Utf8 j #20 = Utf8 I #21 = Utf8 i #22 = Utf8 args #23 = Utf8 [Ljava/lang/String; #24 = Utf8 StackMapTable #25 = Utf8 SourceFile #26 = Utf8 ThreadDemo.java #27 = NameAndType #10:#11 // "":()V #28 = Utf8 java/lang/Thread #29 = Utf8 com/gsy/utildemo/thread/api/ThreadDemo$1 #30 = NameAndType #10:#35 // "":(I)V #31 = NameAndType #10:#36 // "":(Ljava/lang/Runnable;)V #32 = NameAndType #37:#11 // start:()V #33 = Utf8 com/gsy/utildemo/thread/api/ThreadDemo #34 = Utf8 java/lang/Object #35 = Utf8 (I)V #36 = Utf8 (Ljava/lang/Runnable;)V #37 = Utf8 start { public com.gsy.utildemo.thread.api.ThreadDemo(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/gsy/utildemo/thread/api/ThreadDemo; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=5, locals=3, args_size=1 0: iconst_0 1: istore_1 2: iload_1 3: bipush 10 5: if_icmpge 34 8: iload_1 9: istore_2 10: new #2 // class java/lang/Thread 13: dup 14: new #3 // class com/gsy/utildemo/thread/api/ThreadDemo$1 17: dup 18: iload_2 19: invokespecial #4 // Method com/gsy/utildemo/thread/api/ThreadDemo$1."":(I)V 22: invokespecial #5 // Method java/lang/Thread."":(Ljava/lang/Runnable;)V 25: invokevirtual #6 // Method java/lang/Thread.start:()V 28: iinc 1, 1 31: goto 2 34: return LineNumberTable: line 7: 0 line 8: 8 line 9: 10 line 14: 25 line 7: 28 line 17: 34 LocalVariableTable: Start Length Slot Name Signature 10 18 2 j I 2 32 1 i I 0 35 0 args [Ljava/lang/String; StackMapTable: number_of_entries = 2 frame_type = 252 /* append */ offset_delta = 2 locals = [ int ] frame_type = 250 /* chop */ offset_delta = 31 }

class文件大致分为以下几部分:

文件元数据 类信息(类名、继承/实现关系、编译器版本、访问控制修饰符) 常量池 属性信息(私有属性信息不在内) 方法信息(签名、访问控制符、代码(指令、异常表、行号表(原码:字节码)、本地变量表、栈图))
由于ThreadDemo类没有属性信息,莫问直接从方法的code部分读: 1.1 init()初始化方法 aload_0指令值将本地变量表中0 slot位的引用加载到操作数栈顶。此时本地变量表0位时this引用,这是因为构造方法内部要通过this来调用父类引用,所以编译器加了this;反之,方法没有用到this,其不会被放入本地变量表0位; invokespecial该指令用来调用实例构造器方法, 私有方法和父类方法;通过栈中this引用调用完成后连同参数一起弹出,这里调用的是无参函数,仅弹出this; return 返回指令
实际上编译器会将属性赋值、示例代码块代码加入到初始化方法中,所以经过编译器处理的构造方法如下:
初始化方法 = 父类初始化方法(super) + 属性赋值 + 实例代码块 + 构造方法内部代码 1.2 mian方法 iconst_0 将int常量0压入操作数栈; istore_1 将操作数栈头部弹出,存入本地变量表中slot_1的位置,这里该位置代表变量i; iload_1 将本地变量表中的slot_1位置的值压入操作数栈,即变量i的值; bipush 10 将byte常量10转为int常量并压入操作数栈; if_icmpge 34 比较操作数栈前两位,并弹出;满足前大于等与后(即栈顶元素)则跳转到34指令; iload_1 将本地变量表中的slot_1位置的值压入操作数栈,即变量i的值; istore_2 将操作数栈头部弹出,存入本地变量表中slot_2的位置,这里该位置代表变量j; new #2 创建生成Thread对象,将对象引用压入操作数栈 dup 将栈顶元素复制一份后再压入操作数栈;new指令后跟着dup,因为要调用出书化方法,会消耗一个引用,不复制的的话在初始化后,无法进行引用赋值或作为参数传递或方法调用,该引用会丢失;这里是用作**start()**方法调用; new #3 创建生成ThreadDemo$1对象,将对象引用压入操作数栈; dup 同上步;引用用作参数传递; iload_2 将slot_2的int变量加载到操作数栈; invokespecial #4 特殊方法调用(这里调用的是内部类构造,但其调用的是有参构造);弹出前n+1个操作数,底部是方法调用对象的引用,后面n个对应参数列表;这一步弹出的是栈顶的参数 j = 10和一个ThreadDemo$1对象引用; invokespecial #5 特殊方法调用(这里是Thread类的有参构造);弹出一个ThreadDemo$1对象引用和一个Thread对象引用; invokevirtual #6 调用**start()**方法;弹出剩下最后一个Thread对象引用; iinc 1,1 将slot_1位置加int变量加1;这里因为i++没有后续操作,不需要iload指令。但其实平时要是有其它操作,一般需要将slot加载到操作数栈用作运算;i++后接运算符:先iload后innc;++i后接运算符:先iinc后iload goto 2 跳转到指令2; return 返回 2.ThreadDemo$1.class Classfile /D:/work/workspace/util-demo/src/main/java/com/gsy/utildemo/thread/api/ThreadDemo$1.class Last modified 2020-3-22; size 950 bytes MD5 checksum d0c952ff06f40d24aa9f6d4bc76af6b0 Compiled from "ThreadDemo.java" final class com.gsy.utildemo.thread.api.ThreadDemo$1 implements java.lang.Runnable minor version: 0 major version: 52 flags: ACC_FINAL, ACC_SUPER Constant pool: #1 = Fieldref #11.#31 // com/gsy/utildemo/thread/api/ThreadDemo$1.val$j:I #2 = Methodref #12.#32 // java/lang/Object."":()V #3 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream; #4 = Class #35 // java/lang/StringBuilder #5 = Methodref #4.#32 // java/lang/StringBuilder."":()V #6 = String #36 // 绾跨▼ #7 = Methodref #4.#37 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #8 = Methodref #4.#38 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; #9 = Methodref #4.#39 // java/lang/StringBuilder.toString:()Ljava/lang/String; #10 = Methodref #40.#41 // java/io/PrintStream.println:(Ljava/lang/String;)V #11 = Class #42 // com/gsy/utildemo/thread/api/ThreadDemo$1 #12 = Class #43 // java/lang/Object #13 = Class #44 // java/lang/Runnable #14 = Utf8 val$j #15 = Utf8 I #16 = Utf8 #17 = Utf8 (I)V #18 = Utf8 Code #19 = Utf8 LineNumberTable #20 = Utf8 LocalVariableTable #21 = Utf8 this #22 = Utf8 InnerClasses #23 = Utf8 Lcom/gsy/utildemo/thread/api/ThreadDemo$1; #24 = Utf8 run #25 = Utf8 ()V #26 = Utf8 SourceFile #27 = Utf8 ThreadDemo.java #28 = Utf8 EnclosingMethod #29 = Class #45 // com/gsy/utildemo/thread/api/ThreadDemo #30 = NameAndType #46:#47 // main:([Ljava/lang/String;)V #31 = NameAndType #14:#15 // val$j:I #32 = NameAndType #16:#25 // "":()V #33 = Class #48 // java/lang/System #34 = NameAndType #49:#50 // out:Ljava/io/PrintStream; #35 = Utf8 java/lang/StringBuilder #36 = Utf8 绾跨▼ #37 = NameAndType #51:#52 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; #38 = NameAndType #51:#53 // append:(I)Ljava/lang/StringBuilder; #39 = NameAndType #54:#55 // toString:()Ljava/lang/String; #40 = Class #56 // java/io/PrintStream #41 = NameAndType #57:#58 // println:(Ljava/lang/String;)V #42 = Utf8 com/gsy/utildemo/thread/api/ThreadDemo$1 #43 = Utf8 java/lang/Object #44 = Utf8 java/lang/Runnable #45 = Utf8 com/gsy/utildemo/thread/api/ThreadDemo #46 = Utf8 main #47 = Utf8 ([Ljava/lang/String;)V #48 = Utf8 java/lang/System #49 = Utf8 out #50 = Utf8 Ljava/io/PrintStream; #51 = Utf8 append #52 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder; #53 = Utf8 (I)Ljava/lang/StringBuilder; #54 = Utf8 toString #55 = Utf8 ()Ljava/lang/String; #56 = Utf8 java/io/PrintStream #57 = Utf8 println #58 = Utf8 (Ljava/lang/String;)V { final int val$j; descriptor: I flags: ACC_FINAL, ACC_SYNTHETIC com.gsy.utildemo.thread.api.ThreadDemo$1(int); descriptor: (I)V flags: Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #1 // Field val$j:I 5: aload_0 6: invokespecial #2 // Method java/lang/Object."":()V 9: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/gsy/utildemo/thread/api/ThreadDemo$1; public void run(); descriptor: ()V flags: ACC_PUBLIC Code: stack=3, locals=1, args_size=1 0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 3: new #4 // class java/lang/StringBuilder 6: dup 7: invokespecial #5 // Method java/lang/StringBuilder."":()V 10: ldc #6 // String 线程 12: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 15: aload_0 16: getfield #1 // Field val$j:I 19: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder; 22: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 25: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 28: return LineNumberTable: line 11: 0 line 12: 28 LocalVariableTable: Start Length Slot Name Signature 0 29 0 this Lcom/gsy/utildemo/thread/api/ThreadDemo$1; } 这里需要注意由于源代码在匿名内部类中使用了方法中的局部变量j,这里会给匿名内部类生成一个val$j属性, 并自动生成有参构造为属性赋值;(这里比较特别,使用匿名内部类是main方法,它是静态的,也没有访问外部 类任何属性,所以内部类不需要持有外部类对象引用,不需要内部类构造参数中加一个外部类对象的引用)扩展 一下,如果不仅使用了局部变量,还使用了外部类属性,会怎么样?实际上编译器会在外部内生成一个静态方法 (*) static access$$xxx(ThreadDemo),这个方法传入外部类对象引用,里面通过引用返回访问的属性; 同时构造内部类也会增加一个外部类ThreadDemo类型的引用,对应的构造方法会增加一个参数为ThreadDemo类 型的,完成属性赋值。 2.1 init(int)初始化方法 aload_0 加载slot_0位置的this引用到操作数栈; iload_1 加载slot_1位置变量int型变量到操作数栈,这里不知道为什么,在本地变量表没有展示,实际上slot_1是方法的形参; putfield #1 给属性赋值,用this访问属性,赋值为栈顶值1;弹出栈顶两个元素; aload_0 加载slot_0位置的this引用到操作数栈; invokespecial #2 调用父类的初始化方法;弹出栈顶元素; return 返回
这里有一个疑问,为什么父类初始化在属性赋值之后?欢迎答疑! 2.2 run()方法 getstatic #3 获得静态属性,并压入栈中;这里指的是系统的输出流引用; new #4 生成对象;这里是StringBuilder对象; dup 栈顶复制引用;用作构造方法的调用; invokespecial #5 调用初始化方法;弹出栈顶的StringBuilder引用; ldc #6 加载字符串常量到栈顶; invokevirtual #7 调用虚方法;这里是append方法,有参,返回自身引用;方法调用先弹出栈顶的引用和参数,然后将返回值压入栈,这里是拼接了常量字符串的StringBuliler引用; aload_0 加载slot_0的this引用到栈顶; getfield #1 获得属性值;操作数栈弹出this引用,将属性值压入栈;这里是val$j的值; invokevirtual #8 调用append(I)函数;弹出栈顶的StringBuilder引用和参数,将返回结果拼接完全的字符串压入栈中;此时栈中剩下一个输出流引用和一个StringBuilder引用; invokevirtual #9 调用StringBuilder的toString方法;弹出StringBuilder引用,将方法返回值String对象引用压入栈中;此时栈中剩下一个输出流引用和一个String对象引用; invokevirtual #10 调用输出流的print(String)方法;弹出最后两个元素; return 返回;

我们尝试以匿名内部类为例解读了编译器生成的字节码文件(反编译后的),但这里面依然有许多的偶然性,需要大家探索;比如:可以将main方法改为一个非静态方法,就会发现内部类会增加一个外部类引用的属性,即使内部类没有访问外部类的任何属性;而本例比较特殊:匿名内部类使用在一个静态方法中,并且匿名内部类没有访问外部类的任何属性,所以在内部类中没有添加外部类属性;

3.结论与疑问 3.1结论 在方法中使用匿名内部类时,匿名内部类中访问方法的局部变量使用过为匿名内部类增加一个该类型属性,同时外部类会调用有参构造传入改值完成赋值来实现的; 如果是在非静态方法中使用匿名内部类,无论是否匿名内部类是否访问外部类属性,内部类都会增加一个外部类类型的属性,同时在外部类生成一个静态有参方法access$xx(外部类引用);匿名内部类访问外部类属性的就是通过这种方法 ,将外部类引用传入,通过返回值访问到外部类的属性; 3.2疑问 为什在本例中的匿名内部类的构造函数中,会出现先属性赋值后嗲用父类后构造的情况?
作者:叫我小宫



匿名内部类 内部类 JAVA 字节码

需要 登录 后方可回复, 如果你还没有账号请 注册新账号