从字节码层面理解泛型

命令行

//编译成 class 文件
javac Test.java 

//反汇编 class 文件
javap -V Test.class

Android Studio 编译的 class
文件位于 build/intermediates/clases/debug/包名

IDEA 插件

  • jclasslib Bytecode viewer

  • ASM Bytecode Viewer

这两款插件都可以在 Android Studio Plugins 里直接下载安装

字节码的组成

方法调用在JVM中转换成的是字节码执行,字节码指令执行的数据结构就是栈帧。

栈帧的数据结构主要分为四个部分:局部变量表、操作数栈、动态链接以及方法返回地址(包括正常调用和异常调用的完成结果)。

局部变量表(local variables)

当方法被调用时,参数会传递到从0开始的连续的局部变量表的索引位置上。local variables的最大长度是在编译期间决定的。一个局部变量表的占用了32位的存储空间(一个存储单位称之为slot,槽),所以可以存储一个boolean、byte、char、short、float、int、refrence和returnAdress数据,long和double需要2个连续的局部变量表来保存,通过较小位置的索引来获取。如果被调用的是实例方法,那么第0个位置存储“this”关键字代表当前实例对象的引用。

操作数栈

操作数栈同局部变量表一样,也是编译期间就能决定了其存储空间(最大的单位长度)。

操作数栈是在JVM字节码执行一些指令(第二部分会介绍一些指令集)时创建的,主要是把局部变量表中的变量压入操作数栈,在操作数栈中进行字节码指令的操作,再将变量出操作数栈,结果入操作数栈。

动态链接

每个栈帧指向运行时常量池中该栈帧所属的方法的引用,也就是字节码的发放调用的引用。动态链接就是将符号引用所表示的方法,转换成方法的直接引用。加载阶段或第一次使用时转化为直接引用的(将变量的访问转化为访问这些变量的存储结构所在的运行时内存位置)就叫做静态解析。JVM的动态链接还支持运行期转化为直接引用。也可以叫做Late Binding,晚期绑定。

方法返回地址

方法正常退出会把返回值压入调用者的栈帧的操作数栈,PC计数器的值就会调整到方法调用指令后面的一条指令。这样使得当前的栈帧能够和调用者连接起来,并且让调用者的栈帧的操作数栈继续往下执行。

方法的异常调用完成,主要是JVM抛出的异常,如果异常没有被捕获住,或者遇到athrow字节码指令显示抛出,那么就没有返回值给调用者。

Java 代码:

public class MyTest {
    private int myNum = 20;

    public void func() {
        myNum = 50;
    }
}

编译后的 class 源文件:

cafe babe 0000 0033 0015 0a00 0400 1109
0003 0012 0700 1307 0014 0100 056d 794e
756d 0100 0149 0100 063c 696e 6974 3e01
0003 2829 5601 0004 436f 6465 0100 0f4c
696e 654e 756d 6265 7254 6162 6c65 0100
124c 6f63 616c 5661 7269 6162 6c65 5461
626c 6501 0004 7468 6973 0100 1a4c 636f
6d2f 7961 7a68 6964 6576 2f64 656d 6f2f
4d79 5465 7374 3b01 0004 6675 6e63 0100
0a53 6f75 7263 6546 696c 6501 000b 4d79
5465 7374 2e6a 6176 610c 0007 0008 0c00
0500 0601 0018 636f 6d2f 7961 7a68 6964
6576 2f64 656d 6f2f 4d79 5465 7374 0100
106a 6176 612f 6c61 6e67 2f4f 626a 6563
7400 2100 0300 0400 0000 0100 0200 0500
0600 0000 0200 0100 0700 0800 0100 0900
0000 3900 0200 0100 0000 0b2a b700 012a
1014 b500 02b1 0000 0002 000a 0000 000a
0002 0000 0007 0004 0008 000b 0000 000c
0001 0000 000b 000c 000d 0000 0001 000e
0008 0001 0009 0000 0035 0002 0001 0000
0007 2a10 32b5 0002 b100 0000 0200 0a00
0000 0a00 0200 0000 0b00 0600 0c00 0b00
0000 0c00 0100 0000 0700 0c00 0d00 0000
0100 0f00 0000 0200 10

javap 反汇编后的代码:

Classfile /Users/zengyazhi/Documents/zyzdev/AndroidDemo/app/build/intermediates/classes/debug/com/yazhidev/demo/MyTest.class
  Last modified 2018-12-28; size 393 bytes
  MD5 checksum 2872209fbe3efb46c70b23bf85be75fd
  Compiled from "MyTest.java"
public class com.yazhidev.demo.MyTest
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#18         // com/yazhidev/demo/MyTest.myNum:I
   #3 = Class              #19            // com/yazhidev/demo/MyTest
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               myNum
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/yazhidev/demo/MyTest;
  #14 = Utf8               func
  #15 = Utf8               SourceFile
  #16 = Utf8               MyTest.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // myNum:I
  #19 = Utf8               com/yazhidev/demo/MyTest
  #20 = Utf8               java/lang/Object
{
  public com.yazhidev.demo.MyTest();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        20
         7: putfield      #2                  // Field myNum:I
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/yazhidev/demo/MyTest;

  public void func();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: bipush        50
         3: putfield      #2                  // Field myNum:I
         6: return
      LineNumberTable:
        line 11: 0
        line 12: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/yazhidev/demo/MyTest;
}
SourceFile: "MyTest.java"

操作码

opcode(指令) = 操作码 + 操作数

例如 bipush 10 这是一条指令,是由操作码 bipush 后跟一个操作数 10 组成,该指令的作用是将整型数 10 压到操作数栈中。

  • aload_0(指令码:0x2a)

    从局部变量数组中加载一个对象引用到操作数栈的栈顶,最后的数字对应的是局部变量数组中的位置,只能是0,1,2,3。(第一个局部变量是this引用)

  • invokespecial(0xb7)

    只能调用三类方法:<init>方法;私有方法;super.method()。因为这三类方法的调用对象在编译时就可以确定

  • invokevirtual(0xb6)

    是一种动态分派的调用指令

  • bipush(0x10)

    用来把一个字节作为整型压到操作数栈中

  • putfield(0xb5)

    后面跟一个操作数(该操作数引用的是运行时常量池里的一个字段,在这里这个字段是 myNum),将栈顶的值赋给这个。赋给这个字段的值,以及包含这个字段的对象引用,在执行这条指令的时候,都会从操作数栈顶上 pop 出来

  • ldc(0x12)

    常量池中的常量值入栈

  • CHECKCAST(0xc0)

    类型强转

部分字节码指令集可见:

《Java二进制指令代码解析》

《JVM 虚拟机字节码指令表》

解析

回到上面 MyTest 的构造函数里:

0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush        20
7: putfield      #2                  // Field myNum:I
10: return

ASM Bytecode viewer 显示的字节码为:

// class version 51.0 (51)
// access flags 0x21
public class com/yazhidev/demo/MyTest {

  // compiled from: MyTest.java

  // access flags 0x2
  private I myNum

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    ALOAD 0
    BIPUSH 20
    PUTFIELD com/yazhidev/demo/MyTest.myNum : I
    RETURN
   L2
    LOCALVARIABLE this Lcom/yazhidev/demo/MyTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  public func()V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    BIPUSH 50
    PUTFIELD com/yazhidev/demo/MyTest.myNum : I
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/yazhidev/demo/MyTest; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

从字节码看泛型

Java 的泛型是完全在编译器中实现的,由编译器执行类型检查和类型推断,然后生成普通的非泛型的字节码,虚拟机完全不感知泛型的存在。编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除。

Java 代码:

public class Generic<T> {

    private T data;

    public T get() {
        return data;
    }

    public void set(T data) {
        this.data = data;
    }
}

从生成的字节码中可以看到,泛型 T 已经被擦除了:

private Ljava/lang/Object; data

public getData()Ljava/lang/Object;

public setData(Ljava/lang/Object;)V

类型擦除与多态冲突的问题

子类 B,指定了泛型类型:

public class B extends Generic<Number> {

    private Number n;

    public Number get() {
        return n;
    }

    public void set(Number n) {
        this.n = n;
    }
}

子类 C,未指定泛型类型:

public class C extends Generic {

    private Number n;

    public Number get() {
        return n;
    }

    public void set(Number n) {
        this.n = n;
    }
}

我们在写 B 类时,指定了泛型类型为 Number,对于 B 类的方法 get()Numberset(Number),我们的本意应该是对父类的 get()Tset(T) 方法进行重写。但上面我们知道了,父类的 get()Tset(T) 在字节码中实际上是 get()Objectset(Object),与类 B 的方法 set(Number) 方法参数不一样,理论上应该算重载而不是重写。为了解决这一冲突,JVM 采用了一种特殊的方法:桥接。

我们先看 B 类的字节码:

public get()Ljava/lang/Number;
public set(Ljava/lang/Number;)V

// access flags 0x1041
public synthetic bridge set(Ljava/lang/Object;)V
    L0
    LINENUMBER 7 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST java/lang/Number
    INVOKEVIRTUAL com/yazhidev/demo/B.set (Ljava/lang/Number;)V
    RETURN
    L1
    LOCALVARIABLE this Lcom/yazhidev/demo/B; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 2

// access flags 0x1041
public synthetic bridge get()Ljava/lang/Object;
    L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKEVIRTUAL com/yazhidev/demo/B.get ()Ljava/lang/Number;
    ARETURN
    L1
    LOCALVARIABLE this Lcom/yazhidev/demo/B; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

可以发现编译器自动生成了 set(Object)get()Object 两个桥接方法来重写父类方法,同时这两个桥接方法实际上调用了对应的 set(Number) 方法和 get()Number 方法。虚拟机通过使用桥接方法,来解决了类型擦除和多态的冲突。对于开发者来说,对于指定了泛型类型为 Number 的 B 类来说,其 set(Number) 方法就是对父类方法 set(T) 的重写,同理 get()Number 也是对父类方法 get()T 的重写。

但 C 类则有些不同,C 类未指定泛型类型,所以父类中的方法为 get()Objectset(Object),C 类中的 set(Number) 与父类 set(Object) 方法参数不同,理所当然是重载,我们都知道,只有返回值不同不满足重载条件,所以对 C 类的 get()Number 方法来说,应该算是对父类方法 get()T 的重写。

我们来看 C 类的字节码:

public get()Ljava/lang/String;
public set(Ljava/lang/String;)V

// access flags 0x1041
public synthetic bridge get()Ljava/lang/Object;
    L0
    LINENUMBER 7 L0
    ALOAD 0
    INVOKEVIRTUAL com/yazhidev/demo/C.get ()Ljava/lang/String;
    ARETURN
    L1
    LOCALVARIABLE this Lcom/yazhidev/demo/C; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

可以发现编译期自动生成了 get()Object 桥接方法来重写父类方法。但我们发现字节码里却同时存在了两个只有返回值类型不同的同名方法,这是为什么呢?

这里就需要提到方法特征签名,只有特征签名不同的方法才可以共存。

  • Java 层方法签名 = 方法名 + 参数类型 + 参数顺序

    所以在 Java 语言里,重载一个方法需要两个同名方法的参数类型不同,或者参数顺序不同,只有返回值类型不同是无法通过编译的。

  • JVM 层方法签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型 + 可能抛出的异常

    所以在 class 文件里,是可以存在两个只有返回值类型不同的同名方法。也就是上面的 get()Objectget()Number

参考

《描述符与特征签名的区别》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容