JVM_字节码文件(ClassFile)详解

2021-12-05  本文已影响0人  wo883721

我们知道javac 命令可以将 .java 文件编译成 .class 文件,而这个Class 文件 中包含了Java虚拟机指令集、符号表以及若干其他辅助信息;最终将在Java虚拟机运行。

  • Java虚拟机最终运行的是 Class 文件,它其实不管你是不是由 .java 文件生成的,如果有别的程序语言能将自己程序逻辑翻译成一个Class 文件,那么它也可以运行在 Java虚拟机 上的。
  • 也就是说Java虚拟机其实并不和 java 语言强绑定,它只与Class 这个二进制文件强关联,Class 文件包含程序运行的所有信息。

本文是以 JVM8 为例的。

一. class 文件格式

每一个 Class文件都有如下的 ClassFile 文件结构:

ClassFile {
   u4 magic;
   u2 minor_version;
   u2 major_version;
   u2 constant_pool_count;
   cp_info constant_pool[constant_pool_count-1];
   u2 access_flags;
   u2 this_class;
   u2 super_class;
   u2 interfaces_count;
   u2 interfaces[interfaces_count];
   u2 fields_count;
   field_info fields[fields_count];
   u2 methods_count;
   method_info methods[methods_count];
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}
  • 每一个Class文件都对应着唯一一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(比如类或接口也可以通过类加载器直接生成)。
  • Class文件 并不一定是一个文件,它指的是是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。多字节数据项总是按照 big-endian(大端在前)的顺序存储。
  • Class文件 有两种数据格式:
    • 一种是固定字节数的数据结构,称之为(Items),其实就是无符号数。例如上图 u2 表示两个字节无符号数(范围 0 -> 65536)。
    • 一种是可变长度字节数的数据结构,称之为(Tables),用来记录复合数据结构,都是以 _info 结尾。例如上图中的 cp_info,field_info,method_infoattribute_info

先简单介绍一下 ClassFile 文件结构各部分含义:

二. 各类名称在 Class 文件 中的表示形式

三. 描述符

描述符是表示字段或方法类型的字符串。

3.1 字段描述符

字段描述符表示类、实例或局部变量的类型。

字段描述符语法:

1. FieldDescriptor -> FieldType
2. FieldType -> BaseType | ObjectType | ArrayType
3. BaseType -> B | C | D | F | I | J | S | Z
4. ObjectType -> LClassName;
5. ArrayType -> [ComponentType
6. ComponentType -> FieldType

这个语法中,B C D F I J S Z L ; [ 是终结符,其他的都是非终结符。这个方面不清楚的请看我的 编译原理-文法定义 相关文章说明。

从上面文法可以看出,字段描述符中一共有三个类型:

3.2 方法描述符

方法描述符包含0个或者多个参数描述符以及一个返回值描述符。

方法描述符语法:

1. MethodDescriptor -> ({ParameterDescriptor}) ReturnDescriptor
2. ParameterDescriptor-> FieldType
3. ReturnDescriptor-> FieldType | VoidDescriptor
4. VoidDescriptor-> V
  • 这个语法中 ( ) V 这个三个字符是终结符。
  • {ParameterDescriptor} 中的 { } 表示这个非终结符 ParameterDescriptor 能够出现0次或者多次。
  • V 表示没有任何返回值,就是 void 关键字。
  • 例如方法 String test(int i, long l, Integer i1, Long l1, Object[] objs) {..} 对应的描述符 (IJLjava/lang/Integer;Ljava/lang/Long;[Ljava/lang/Object;)Ljava/lang/String;

看了描述符,可能大家有点疑惑,泛型信息怎么表示啊?

描述符的确不能记录泛型相关信息,泛型信息记录在 Signatures 属性中。

四. 常量池

常量池的通用格式如下:

cp_info {
   u1 tag;
   u1 info[];
}

一个字节无符号数 tag 表示常量类型;info 表示不同常量类型的数据。

目前 JVM8 中一共用14 种常量类型,分别如下:

Constant Type Value 描述
CONSTANT_Utf8 1 表示 Utf8 编码的字符串
CONSTANT_Integer 3 表示整形字面量
CONSTANT_Float 4 表示单精度浮点型字面量
CONSTANT_Long 5 表示长整形字面量
CONSTANT_Double 6 表示双精度浮点型字面量
CONSTANT_Class 7 表示类或者接口的符号引用
CONSTANT_String 8 表示字符串类型字面量
CONSTANT_Fieldref 9 表示字段的符号引用
CONSTANT_Methodref 10 表示类中方法的符号引用
CONSTANT_InterfaceMethodref 11 表示接口中方法的符号引用
CONSTANT_NameAndType 12 表示字段或者方法的名称和描述符
CONSTANT_MethodHandle 15 表示方法的句柄
CONSTANT_MethodType 16 表示方法的类型
CONSTANT_InvokeDynamic 18 表示动态计算常量
CONSTANT_Utf8_info {
   u1 tag;
   u2 length;
   u1 bytes[length];
}
CONSTANT_Integer_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Float_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Long_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
CONSTANT_Double_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
CONSTANT_Class_info {
   u1 tag;
   u2 name_index;
}
CONSTANT_String_info {
   u1 tag;
   u2 string_index;
}
CONSTANT_Fieldref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_Methodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_NameAndType_info {
   u1 tag;
   u2 name_index;
   u2 descriptor_index;
}
CONSTANT_MethodHandle_info {
   u1 tag;
   u1 reference_kind;
   u2 reference_index;
}
CONSTANT_MethodType_info {
   u1 tag;
   u2 descriptor_index;
}
CONSTANT_InvokeDynamic_info {
   u1 tag;
   u2 bootstrap_method_attr_index;
   u2 name_and_type_index;
}

4.1 CONSTANT_Utf8_info

CONSTANT_Utf8_info {
   u1 tag;
   u2 length;
   u1 bytes[length];
}

4.2 CONSTANT_Integer_infoCONSTANT_Float_info

CONSTANT_Integer_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Float_info {
   u1 tag;
   u4 bytes;
}

4.3 CONSTANT_Long_infoCONSTANT_Double_info

CONSTANT_Long_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
CONSTANT_Double_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}

4.4 CONSTANT_Class_info

CONSTANT_Class_info {
   u1 tag;
   u2 name_index;
}

4.5 CONSTANT_String_info

CONSTANT_String_info {
   u1 tag;
   u2 string_index;
}

4.6 CONSTANT_Fieldref_info , CONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info

CONSTANT_Fieldref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_Methodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}

我们知道要使用一个字段或者调用一个方法,就必须知道字段或者方法所属类符号引用,和字段的名字和类型,方法的名字和方法参数类型以及方法返回值类型。
但是我们知道类是能继承的,那么子类调用父类的方法或者字段,这里的所属类符号引用,到底是子类本身还是父类的呢?

请大家思考一下,后面的例子中,我们将会讲解。

4.7 CONSTANT_NameAndType_info

CONSTANT_NameAndType_info {
   u1 tag;
   u2 name_index;
   u2 descriptor_index;
}

4.8 CONSTANT_MethodHandle_info

CONSTANT_MethodHandle_info {
   u1 tag;
   u1 reference_kind;
   u2 reference_index;
}

4.9 CONSTANT_MethodType_info

CONSTANT_MethodType_info {
   u1 tag;
   u2 descriptor_index;
}

4.10 CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info {
   u1 tag;
   u2 bootstrap_method_attr_index;
   u2 name_and_type_index;
}

五. 访问标志(access_flags)

我们知道类,方法,字段都有不同的访问标志,在Class 文件 中使用一个u2 类型数据项来存储,也就是最多可以有 16 个不同标志位。
在类,方法,字段中有相同的标志,也有不同的标志,总体规划,我们可以借助 Modifier 类的源码来了解:

    public static final int PUBLIC           = 0x00000001;
    public static final int PRIVATE          = 0x00000002;
    public static final int PROTECTED        = 0x00000004;
    public static final int STATIC           = 0x00000008;
    public static final int FINAL            = 0x00000010;
    public static final int SYNCHRONIZED     = 0x00000020;
    public static final int VOLATILE         = 0x00000040;
    public static final int TRANSIENT        = 0x00000080;
    public static final int NATIVE           = 0x00000100;
    public static final int INTERFACE        = 0x00000200;
    public static final int ABSTRACT         = 0x00000400;
    public static final int STRICT           = 0x00000800;
    static final int BRIDGE      = 0x00000040;
    static final int VARARGS     = 0x00000080;
    static final int SYNTHETIC   = 0x00001000;
    static final int ANNOTATION  = 0x00002000;
    static final int ENUM        = 0x00004000;
    static final int MANDATED    = 0x00008000;
  • 因为 access_flags 是两个字节数,这里使用 int 类型,也就说前面4 个永远是 0(0x0000----)。
  • 这里 0x000000400x00000080 重复使用了,但是没关系,因为表示不同的访问标志。

5.1 类的访问标志

Modifier 类中,类的访问标志:

    private static final int CLASS_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.FINAL   |
        Modifier.STRICT;

    private static final int INTERFACE_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.STRICT;

我们知道在 java 中类可以用的修饰符有: public,protected,private,abstract,static,final,strictfp

  • 其中 protected,privatestatic 都是只能用在内部类里面。
  • strictfp 这个关键字表示这个类精确进行浮点运算。不过这么多年也没有看谁用过。
  • final 关键字不能用在接口上。

但是我们再看 Class 文件 中类的访问标志:

标志名 描述
ACC_PUBLIC 0x0001 声明为 public,可以从包外访问
ACC_FINAL 0x0010 声明为 final,不允许被继承
ACC_SUPER 0x0020 为了兼容之前编译器编译代码而设置的,目前编译器编译的代码,这个标志位都是1
ACC_INTERFACE 0x0200 表示是类还是接口
ACC_ABSTRACT 0x0400 声明为 abstract,不能被实例化
ACC_SYNTHETIC 0x1000 声明为 synthetic,表示这个 Class 文件 不在源代码中
ACC_ANNOTATION 0x2000 表示为注解类型
ACC_ENUM 0x4000 表示为枚举类型

仔细看,你会发现有些不同点:

5.2 字段的访问标志

Modifier 类中,字段的访问标志:

    private static final int FIELD_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.STATIC         | Modifier.FINAL        | Modifier.TRANSIENT |
        Modifier.VOLATILE;

我们知道在 java 中字段可以用的修饰符有: public,protected,private,static,final,transientvolatile

  • 其中 transient 表示这个字段是瞬时,进行java 序列化的时候,不会序列化被transient修饰的字段。
  • volatile 表示这个字段是可见的。volatile关键字的详细介绍请看 Java多线程详细介绍

但是我们再看 Class 文件 中字段的访问标志:

标志名 描述
ACC_PUBLIC 0x0001 声明为 public,可以从包外访问
ACC_PRIVATE 0x0002 声明为 private,只能在定义该字段的类中访问
ACC_PROTECTED 0x0004 声明为 protected,子类可以访问
ACC_STATIC 0x0008 声明为 static
ACC_FINAL 0x0010 声明为 final,表示对象构造完成后,不能直接修改该字段了
ACC_VOLATILE 0x0040 声明为 volatile
ACC_TRANSIENT 0x0080 声明为 transient
ACC_SYNTHETIC 0x1000 表示该字段不是在源码中,由编译器生成
ACC_ENUM 0x4000 表示该字段是枚举(enum)类型

Class 文件 中字段的访问标志和java 中字段的修饰符差不多,只是多了 ACC_SYNTHETICACC_ENUM 两个标志。

5.3 方法的访问标志

Modifier 类中,方法的访问标志:

    private static final int CONSTRUCTOR_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE;

    private static final int METHOD_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.FINAL   |
        Modifier.SYNCHRONIZED   | Modifier.NATIVE       | Modifier.STRICT;

我们知道在 java 中方法可以用的修饰符有:
public,protected,private,abstract,static,final,synchronized, synchronizedstrictfp

但是我们再看 Class 文件 中方法的访问标志:

标志名 描述
ACC_PUBLIC 0x0001 声明为 public,可以从包外访问
ACC_PRIVATE 0x0002 声明为 private,只能在定义该字段的类中访问
ACC_PROTECTED 0x0004 声明为 protected,子类可以访问
ACC_STATIC 0x0008 声明为 static
ACC_FINAL 0x0010 声明为 final, 表示方法不能被覆盖
ACC_SYNCHRONIZED 0x0020 声明为 synchronized, 表示对方法的调用,会包装在同步锁里
ACC_BRIDGE 0x0040 声明为 bridge,由编译器产生
ACC_VARARGS 0x0080 表示方法带有变长参数
ACC_NATIVE 0x0100 声明为 native, 不是由 java 语言实现
ACC_ABSTRACT 0x0400 声明为 abstract ,该方法没有实现方法
ACC_STRICT 0x0800 声明为 strictfp,使用精确浮点模式
ACC_SYNTHETIC 0x1000 该方法由编译器合成的,不是由源码编译出来的

六. 字段和方法

6.1 字段

字段详情 field_info 的格式如下:

field_info {
   u2 access_flags;
   u2 name_index;
   u2 descriptor_index;
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}

6.2 方法

方法详情 method_info 的格式如下:

method_info {
 u2 access_flags;
 u2 name_index;
 u2 descriptor_index;
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}

关于 Class 文件 中属性相关信息,我们再后面章节介绍。

七. 例子

我们可以通过 javap 的命令来阅读 Class 文件 中相关信息。

7.1 最简单的例子

package com.zhang.jvm.reflect.example;

public class T {
}

这个是最简单的一个类,没有任何字段和方法,只继承Object 类,我们来看看它编译后的字节码信息,通过javap -p -v T.class 的命令:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 288 bytes
  MD5 checksum 2771c8258a6734d72812fca914966e07
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // com/zhang/jvm/reflect/example/T
   #3 = Class              #15            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #11 = Utf8               SourceFile
  #12 = Utf8               T.java
  #13 = NameAndType        #4:#5          // "<init>":()V
  #14 = Utf8               com/zhang/jvm/reflect/example/T
  #15 = Utf8               java/lang/Object
{
  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
  • 前面的minor version: 0 之前的都是.class 文件本身的信息,这里魔数magic 信息被省略了。
  • 然后依次就是常量池信息 Constant pool,字段相关信息(因为T.class 没有字段,所以这里没有),方法相关信息({} 里面的,就是T.class默认的构造器方法<init>),最后类属性相关信息(就是这里的SourceFile: "T.java")。
  • this_class,super_class,interfaces 相关信息就在 public class com.zhang.jvm.reflect.example.T 中,Object 父类就隐藏。

我们重点关注常量池相关信息,会发现虽然T.class 很干净,但是也有15 个常量,来我们依次分析:

7.2 有字段和方法的例子

package com.zhang.jvm.reflect.example;
public class T {
    private String name;
    public void test() {}
}

与之前的例子相比较,多了一个字段和方法,那么得到的字节码信息如下:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 388 bytes
  MD5 checksum f97a6c1995036e9605c0121916c3d815
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // com/zhang/jvm/reflect/example/T
   #3 = Class              #18            // java/lang/Object
   #4 = Utf8               name
   #5 = Utf8               Ljava/lang/String;
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #13 = Utf8               test
  #14 = Utf8               SourceFile
  #15 = Utf8               T.java
  #16 = NameAndType        #6:#7          // "<init>":()V
  #17 = Utf8               com/zhang/jvm/reflect/example/T
  #18 = Utf8               java/lang/Object
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"

与之前的相比较,发现有三个变化:

  • 常量池中多了三个常量,都是 CONSTANT_Utf8_info 类型的,分别是 name,Ljava/lang/String;test
  • 多了一个字段name相关信息。
  • 多了一个方法test相关信息。

但是你会发现常量池中怎么没有这个字段nameCONSTANT_Fieldref_info 类型的常量呢?
那是因为我们没有使用这个字段。

package com.zhang.jvm.reflect.example;
public class T {
    private String name;
    public void test() {}
    public void test1() {
        name = "12";
        test(); 
    }
}

多写了一个方法test1 来调用name 字段和 test 方法,那么得到的字节码信息如下:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 499 bytes
  MD5 checksum 2ddae70db9ea9f755f9312eb2b2f2d07
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // 12
   #3 = Fieldref           #5.#22         // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
   #4 = Methodref          #5.#23         // com/zhang/jvm/reflect/example/T.test:()V
   #5 = Class              #24            // com/zhang/jvm/reflect/example/T
   #6 = Class              #25            // java/lang/Object
   #7 = Utf8               name
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #16 = Utf8               test
  #17 = Utf8               test1
  #18 = Utf8               SourceFile
  #19 = Utf8               T.java
  #20 = NameAndType        #9:#10         // "<init>":()V
  #21 = Utf8               12
  #22 = NameAndType        #7:#8          // name:Ljava/lang/String;
  #23 = NameAndType        #16:#10        // test:()V
  #24 = Utf8               com/zhang/jvm/reflect/example/T
  #25 = Utf8               java/lang/Object
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #2                  // String 12
         3: putfield      #3                  // Field name:Ljava/lang/String;
         6: aload_0
         7: invokevirtual #4                  // Method test:()V
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 6
        line 12: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"

与之前的相比较,发现如下变化:

  • 常量池中多了七个常量,分别是字符串 "12" 对应的两个常量#2#21; 字段name 对应的两个常量#3#22;方法test 对应的两个常量#4#23; 以及方法test1 的名称常量#17
  • 多了一个方法test1相关信息。

7.3 继承的例子

package com.zhang.jvm.reflect.example;
public class TParent {
    public String name;
    public void say(){}
}

package com.zhang.jvm.reflect.example;
public class T extends TParent {
    public void test() {
        name = "T";
        say();
    }
}

这里定义一个父类TParent,有一个公共字段name和方法say。子类T 继承TParent类,并有一个方法test 调用父类的字段和方法,来看T 的字节码信息:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 452 bytes
  MD5 checksum aeea52a9b2b166d588e1336dd0a4dcc1
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         // com/zhang/jvm/reflect/example/TParent."<init>":()V
   #2 = String             #18            // T
   #3 = Fieldref           #5.#19         // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
   #4 = Methodref          #5.#20         // com/zhang/jvm/reflect/example/T.say:()V
   #5 = Class              #21            // com/zhang/jvm/reflect/example/T
   #6 = Class              #22            // com/zhang/jvm/reflect/example/TParent
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #14 = Utf8               test
  #15 = Utf8               SourceFile
  #16 = Utf8               T.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Utf8               T
  #19 = NameAndType        #23:#24        // name:Ljava/lang/String;
  #20 = NameAndType        #25:#8         // say:()V
  #21 = Utf8               com/zhang/jvm/reflect/example/T
  #22 = Utf8               com/zhang/jvm/reflect/example/TParent
  #23 = Utf8               name
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               say
{
  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #2                  // String T
         3: putfield      #3                  // Field name:Ljava/lang/String;
         6: aload_0
         7: invokevirtual #4                  // Method say:()V
        10: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
  • 你会发现字段 name 在常量池中#3(Fieldref)和方法 say 在常量池中#4(Methodref),它们所属类都是 T,而不是 TParent
  • 但是你又发现在T的字节码文件中,就没有 name 字段相关信息和 say 方法相关信息。
  • 这个是没有关系的,因为只要父类中相关字段和方法访问权限是可以的,那么子类找不到也会到父类去找的。

但是如果你就想调用父类的该怎么办呢?

这个很好办,我们知道java 中有 super 关键字。

package com.zhang.jvm.reflect.example;
public class T extends TParent {
    public void test() {
        super.name = "T";
        super.say();
    }
}

再来看T的字节码信息:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 452 bytes
  MD5 checksum 7d0901b392b0bfd74300cb87482ba183
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         // com/zhang/jvm/reflect/example/TParent."<init>":()V
   #2 = String             #18            // T
   #3 = Fieldref           #6.#19         // com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
   #4 = Methodref          #6.#20         // com/zhang/jvm/reflect/example/TParent.say:()V
   #5 = Class              #21            // com/zhang/jvm/reflect/example/T
   #6 = Class              #22            // com/zhang/jvm/reflect/example/TParent
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #14 = Utf8               test
  #15 = Utf8               SourceFile
  #16 = Utf8               T.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Utf8               T
  #19 = NameAndType        #23:#24        // name:Ljava/lang/String;
  #20 = NameAndType        #25:#8         // say:()V
  #21 = Utf8               com/zhang/jvm/reflect/example/T
  #22 = Utf8               com/zhang/jvm/reflect/example/TParent
  #23 = Utf8               name
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               say
{
  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #2                  // String T
         3: putfield      #3                  // Field com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
         6: aload_0
         7: invokespecial #4                  // Method com/zhang/jvm/reflect/example/TParent.say:()V
        10: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
  • 我们发现字段 name 在常量池中#3(Fieldref)和方法 say 在常量池中#4(Methodref),它们所属类都变成了 TParent
  • 还有一点需要特别注意的,就是在 test 方法的指令集中,调用 say 方法的指令,从 invokevirtual 指令变成 invokespecial 指令。

子类可以直接使用父类允许访问权限的字段和方法,即使子类中没有相关字段和方法,这个是继承的功效。
但是我们知道面向对象语言,除了继承的特性,还有一个多态特性。

  • 多态就是根据运行时,对象实际类型来调用对应方法,而不是编译时写死的类型去调用,所谓的写死类型就是常量池中 Methodref 类型常量中所属的类型。
  • 我们知道方法是具有多态特性的,那么字段也有多态么。
package com.zhang.jvm.reflect.example;
public class TParent {
    public String name = "TParent";
    public void say(){
        System.out.println("I am TParent");
    }
}

package com.zhang.jvm.reflect.example;
public class T extends TParent {
    public String name = "T";
    public void say(){
        System.out.println("I am T");
    }
    public static void main(String[] agrs) {
        TParent tParent = new T();
        T t = (T) tParent;

        System.out.println("tParent.name: "+ tParent.name+"\nt.name: "+t.name);

        tParent.say();
        t.say();
    }
}

运行结果:

tParent.name: TParent
t.name: T
I am T
I am T

你会发现即使运行期是同一个对象,但是字段name 得到结果是不一样的,即字段是不能多态的;但是方法的确是按照运行期实际对象类型调用。下面看它的字节码信息:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 1070 bytes
  MD5 checksum 3490fbda3bf98c7fbd556ef4c0f5f3f4
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #18.#38        // com/zhang/jvm/reflect/example/TParent."<init>":()V
   #2 = String             #39            // T
   #3 = Fieldref           #7.#40         // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
   #4 = Fieldref           #41.#42        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #43            // I am T
   #6 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #46            // com/zhang/jvm/reflect/example/T
   #8 = Methodref          #7.#38         // com/zhang/jvm/reflect/example/T."<init>":()V
   #9 = Class              #47            // java/lang/StringBuilder
  #10 = Methodref          #9.#38         // java/lang/StringBuilder."<init>":()V
  #11 = String             #48            // tParent.name:
  #12 = Methodref          #9.#49         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #13 = Fieldref           #18.#40        // com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
  #14 = String             #50            // \nt.name:
  #15 = Methodref          #9.#51         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #16 = Methodref          #18.#52        // com/zhang/jvm/reflect/example/TParent.say:()V
  #17 = Methodref          #7.#52         // com/zhang/jvm/reflect/example/T.say:()V
  #18 = Class              #53            // com/zhang/jvm/reflect/example/TParent
  #19 = Utf8               name
  #20 = Utf8               Ljava/lang/String;
  #21 = Utf8               <init>
  #22 = Utf8               ()V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               LocalVariableTable
  #26 = Utf8               this
  #27 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #28 = Utf8               say
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               agrs
  #32 = Utf8               [Ljava/lang/String;
  #33 = Utf8               tParent
  #34 = Utf8               Lcom/zhang/jvm/reflect/example/TParent;
  #35 = Utf8               t
  #36 = Utf8               SourceFile
  #37 = Utf8               T.java
  #38 = NameAndType        #21:#22        // "<init>":()V
  #39 = Utf8               T
  #40 = NameAndType        #19:#20        // name:Ljava/lang/String;
  #41 = Class              #54            // java/lang/System
  #42 = NameAndType        #55:#56        // out:Ljava/io/PrintStream;
  #43 = Utf8               I am T
  #44 = Class              #57            // java/io/PrintStream
  #45 = NameAndType        #58:#59        // println:(Ljava/lang/String;)V
  #46 = Utf8               com/zhang/jvm/reflect/example/T
  #47 = Utf8               java/lang/StringBuilder
  #48 = Utf8               tParent.name:
  #49 = NameAndType        #60:#61        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #50 = Utf8               \nt.name:
  #51 = NameAndType        #62:#63        // toString:()Ljava/lang/String;
  #52 = NameAndType        #28:#22        // say:()V
  #53 = Utf8               com/zhang/jvm/reflect/example/TParent
  #54 = Utf8               java/lang/System
  #55 = Utf8               out
  #56 = Utf8               Ljava/io/PrintStream;
  #57 = Utf8               java/io/PrintStream
  #58 = Utf8               println
  #59 = Utf8               (Ljava/lang/String;)V
  #60 = Utf8               append
  #61 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #62 = Utf8               toString
  #63 = Utf8               ()Ljava/lang/String;
{
  public java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC

  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
         4: aload_0
         5: ldc           #2                  // String T
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void say();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String I am T
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/zhang/jvm/reflect/example/T;

  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: new           #7                  // class com/zhang/jvm/reflect/example/T
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: checkcast     #7                  // class com/zhang/jvm/reflect/example/T
        12: astore_2
        13: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: new           #9                  // class java/lang/StringBuilder
        19: dup
        20: invokespecial #10                 // Method java/lang/StringBuilder."<init>":()V
        23: ldc           #11                 // String tParent.name:
        25: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: aload_1
        29: getfield      #13                 // Field com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
        32: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        35: ldc           #14                 // String \nt.name:
        37: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        40: aload_2
        41: getfield      #3                  // Field name:Ljava/lang/String;
        44: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        47: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        50: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        53: aload_1
        54: invokevirtual #16                 // Method com/zhang/jvm/reflect/example/TParent.say:()V
        57: aload_2
        58: invokevirtual #17                 // Method say:()V
        61: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 15: 13
        line 17: 53
        line 18: 57
        line 19: 61
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      62     0  agrs   [Ljava/lang/String;
            8      54     1 tParent   Lcom/zhang/jvm/reflect/example/TParent;
           13      49     2     t   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"

这个字节码信息比较多,我们主要观察 main 方法的指令集信息:

  • tParent.name 对应第 29 行指令,使用的常量池索引是 #13,是一个Fieldref 类型,所属的类索引就是 TParent
  • tParent.name 对应第 41 行指令,使用的常量池索引是 #3,是一个Fieldref 类型,所属的类索引就是 T
  • tParent.say() 对应第 54 行指令,使用的常量池索引是 #16,是一个Methodref 类型,所属的类索引就是 TParent
  • tParent.say() 对应第 58 行指令,使用的常量池索引是 #17,是一个Methodref 类型,所属的类索引就是 T

主要的区别就是在于 invokevirtual 指令,就是它实现了方法多态的功能,它会根据运行期对象实际类型去匹配对应的方法,而不是根据这里 Methodref 常量中规定所属类去匹配。

7.4 内部类

java 语言中内部类其实是一个非常特殊的存在,里面有很多javac 编译器帮我们做的事情,如下是一个简单的内部类:

public class TS {

    TInner inner = null;

    public void test() {
        inner.name = null;
    }

    class TInner {
        private String name;
    }
}

先看一下TS 的字节码:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/TS.class
  Last modified 2021-12-6; size 637 bytes
  MD5 checksum 5ebdb2d72b0b3d5a224c59860e8b386a
  Compiled from "TS.java"
public class com.zhang.jvm.reflect.example.TS
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#22         // com/zhang/jvm/reflect/example/TS.inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
   #3 = Methodref          #6.#23         // com/zhang/jvm/reflect/example/TS$TInner.access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
   #4 = Class              #24            // com/zhang/jvm/reflect/example/TS
   #5 = Class              #25            // java/lang/Object
   #6 = Class              #26            // com/zhang/jvm/reflect/example/TS$TInner
   #7 = Utf8               TInner
   #8 = Utf8               InnerClasses
   #9 = Utf8               inner
  #10 = Utf8               Lcom/zhang/jvm/reflect/example/TS$TInner;
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/zhang/jvm/reflect/example/TS;
  #18 = Utf8               test
  #19 = Utf8               SourceFile
  #20 = Utf8               TS.java
  #21 = NameAndType        #11:#12        // "<init>":()V
  #22 = NameAndType        #9:#10         // inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
  #23 = NameAndType        #27:#28        // access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
  #24 = Utf8               com/zhang/jvm/reflect/example/TS
  #25 = Utf8               java/lang/Object
  #26 = Utf8               com/zhang/jvm/reflect/example/TS$TInner
  #27 = Utf8               access$002
  #28 = Utf8               (Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
{
  com.zhang.jvm.reflect.example.TS$TInner inner;
    descriptor: Lcom/zhang/jvm/reflect/example/TS$TInner;
    flags:

  public com.zhang.jvm.reflect.example.TS();
    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: aconst_null
         6: putfield      #2                  // Field inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
         9: return
      LineNumberTable:
        line 6: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/zhang/jvm/reflect/example/TS;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
         4: aconst_null
         5: invokestatic  #3                  // Method com/zhang/jvm/reflect/example/TS$TInner.access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
         8: pop
         9: return
      LineNumberTable:
        line 11: 0
        line 12: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/zhang/jvm/reflect/example/TS;
}
SourceFile: "TS.java"
InnerClasses:
     #7= #6 of #4; //TInner=class com/zhang/jvm/reflect/example/TS$TInner of class com/zhang/jvm/reflect/example/TS

在这个类的字节码文件中,你会发现一个很有意思的事情,那就是在 test 方法指令集中,语句 inner.name = null 居然没有对应 putfield 指令,而是 invokestatic 指令,调用了TInner 中一个名为access$002 的类方法。

为什么会这样呢?

  • 那是因为字段都是有访问权限的,而 TInnername 字段的访问权限是private,那么只有这个 TInner 类里面才可以访问它,也就是这个name 字段只能在TInner 类的常量池中生成对应的CONSTANT_field_info 类型常量。
  • 但是java 语言规范里面,外部类又可以调用内部类的私有字段,所以javac 编译器帮我们做了处理,在 TInner 类中生成了一个名为access$002静态方法来给私有字段赋值。

TInner 的字节码不能使用javap 命令看到,我就简单地说一下,有如下重点:

上一篇 下一篇

猜你喜欢

热点阅读