JVM_字节码文件(ClassFile)详解

我们知道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 文件结构各部分含义:

  • magic : Class文件 的魔数,代表这个二进制文件是 Class文件,它的固定是 0xCAFEBABE,不会改变。
  • minor_versionmajor_version : 表示这个 Class文件 的副版本号和主版本号。
  • constant_pool_count : 表示常量池的数量。

    它的类型是 u2,也就是说一个 Class文件最多有65536 -1 个常量。

  • constant_pool[constant_pool_count-1] : 表示常量池,记录这个Class文件所有常量信息。
    • 常量池包含Class文件及其子结构中所有的常量信息。
    • 常量池中的每一项都有一个特征,第一个字节表示这个常量的类型,在 JVM8 中一共有 14 中类型。
    • 常量池的索引范围是 1 -> constant_pool_count -1,也就是constant_pool_count -1 个常量,索引 0 表示不是任何常量。
  • access_flags : 表示这个 Class文件 访问标志。
    • 我们知道在 java 语言中,类,方法,成员字段都可以设置不同的修饰符,这里的访问标志就表示修饰符相关信息。
    • 它的类型是 u2,有16 位二进制数,也就是说它最多可以表示 16 个不同的访问标志。
  • this_class : 表示当前类或者接口的类常量索引。

    它的类型是 u2,就是表示常量池中某项的有效索引值,而且它指向的那个常量必须是 CONSTANT_Class_info 类型。

  • super_class : 表示当前类或者接口的父类常量索引。
    • 它的类型也是 u2,也表示常量池中某项的有效索引值。但是根据当前Class文件是类或者接口,分为两种情况:
      • 如果当前是类的话,当 super_class值是 0,那就表示这个类是 Object 类;其他的情况,super_class值都是常量池中一个CONSTANT_Class_info 类型常量的索引值。
      • 如果当前是接口的话,那么super_class值只会是常量池中一个 CONSTANT_Class_info 类型常量的索引值,而且这个CONSTANT_Class_info 类型表示 Object 类,因为接口的父类就只能是 Object
  • interfaces_count : 表示当前类实现的接口数量,或者当前接口继承的接口数量。

    它的类型是 u2 ,表示最多有 65536 个接口;如果是 0 表示没有任何接口。

  • interfaces[interfaces_count] : 表示接口对应的常量池索引值表,长度就是 interfaces_count
  • fields_count : 表示当前类或者接口的字段数量,包括静态字段和成员字段。
  • fields[fields_count] : 表示当前类或者接口的字段详细信息的表,长度是 fields_count

    它的类型是 field_info,记录着这个字段的所有信息。

  • methods_count : 表示当前类或者接口的方法数量,包括类方法和实例方法。
  • methods[methods_count] : 表示当前类或者接口的方法详细信息的表,长度是 fields_count

    它的类型是 method_info,记录着这个方法的所有信息。

  • attributes_count : 表示当前类或者接口的属性数量。
  • attributes[attributes_count] : 表示当前类或者接口的属性详细信息的表。

    它的类型是 attribute_info,属性的种类非常多,不仅在类中有,在字段详情field_info 和方法详情method_info 中也有相关属性表。这个我们后面会慢慢说明。

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

  • 全限定名

    类和接口的名称都是全限定形式,被称为二进制名称,不过和java 语言中不同,它使用(/) 而不是 (.) 作为分隔符的,例如 java.lang.Object 就变成了 java/lang/Object

  • 非限定名
    • 方法名,字段名,局部变量名和形式参数名都是非限定形式。
    • 非限定名至少有一个 Unicode 字符,但是不能是 ASCII 字符 (. ; [ /) 这四个字符。
    • 除了实例初始化方法 和类初始化方法 以外,其他非限定名也不能出现 (< >) 这两个字符。

三. 描述符

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

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 ; [ 是终结符,其他的都是非终结符。这个方面不清楚的请看我的 编译原理-文法定义 相关文章说明。

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

  • BaseType 表示基础类型。
    • 一共分为八种,分别是 B C D F I J S Z
    • 其中 Z 表示 boolean 类型,因为 B 已经表示byte 类型了;J 表示 long 类型,因为 L 这个字符被引用类型用了。
  • ObjectType 表示引用类型。

    它的格式是 LClassName;,即以L开头,;结尾,中间是类的全限定名。例如Object 类就是 Ljava/lang/Object;

  • ArrayType 表示数组类型。

    它的格式是 [ComponentType,即以[开头,ComponentType 表示三个类型中任意类型。例如 int[] 就是 [I; int[][] 就是 [[I; String[] 就是 [Ljava/lang/String;

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];
}
  • tag : 表示常量类型,值就是1(CONSTANT_Utf8)
  • length : 表示 Utf8 字符串的总字节数。

    它的类型是 u2,也就是 Utf8 字符串最多只能有 65536 个字节。

  • bytes[length] : 表示Utf8 字符串的字节数组数据。

4.2 CONSTANT_Integer_infoCONSTANT_Float_info

CONSTANT_Integer_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Float_info {
   u1 tag;
   u4 bytes;
}
  • tag : 表示常量类型。
    • CONSTANT_Integer_info 的值就是3(CONSTANT_Integer)。
    • CONSTANT_Float_info的值就是 4(CONSTANT_Float)。
  • bytes : 是一个四个字节数,用来储存 intfloat 类型字面量。

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;
}
  • tag : 表示常量类型。
    • CONSTANT_Long_info 的值就是5(CONSTANT_Long)。
    • CONSTANT_Double_info的值就是 6(CONSTANT_Double)。
  • high_byteslow_bytes : 组成一个8个字节数,来储存 longdouble 类型字面量。
  • 特别注意,这两种类型会在常量表中占据两个索引,也就是如果索引 3 处的CONSTANT_Long_info类型,那么下一个有效索引是 5

4.4 CONSTANT_Class_info

CONSTANT_Class_info {
   u1 tag;
   u2 name_index;
}
  • tag : 表示常量类型,CONSTANT_Class_info 的值就是7(CONSTANT_Class)。
  • name_index : 表示类符号引用的索引。

    值必须是当前常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

4.5 CONSTANT_String_info

CONSTANT_String_info {
   u1 tag;
   u2 string_index;
}
  • tag : 表示常量类型,CONSTANT_String_info 的值就是8(CONSTANT_String)。
  • string_index : 表示字符串值的索引。

    值必须是当前常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

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;
}
  • tag : 表示常量类型。
    • CONSTANT_Fieldref_info 的值就是9(CONSTANT_Fieldref);
    • CONSTANT_Methodref_info 的值就是10(CONSTANT_Methodref);
    • CONSTANT_InterfaceMethodref_info 的值就是11(CONSTANT_InterfaceMethodref)。
  • class_index : 表示这个字段或者方法所属类符号引用的索引。
    • 值必须是当前常量池中一个 CONSTANT_Class_info 类型常量的有效索引。
  • name_and_type_index : 表示这个字段或者方法的名称和描述符的索引。
    • 值必须是当前常量池中一个 CONSTANT_NameAndType_info 类型常量的有效索引。
    • 知道字段的名称和描述符,就知道了字段的名字和类型。
    • 知道方法的名称和描述符,就知道了方法的名字和方法参数类型以及方法返回值类型。

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

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

4.7 CONSTANT_NameAndType_info

CONSTANT_NameAndType_info {
   u1 tag;
   u2 name_index;
   u2 descriptor_index;
}
  • tag : 表示常量类型,CONSTANT_NameAndType_info 的值就是12(CONSTANT_NameAndType)。
  • name_index : 表示字段或者方法名称。

    值必须是当前常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

  • descriptor_index : 表示字段或者方法描述符。

    值必须是当前常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

4.8 CONSTANT_MethodHandle_info

CONSTANT_MethodHandle_info {
   u1 tag;
   u1 reference_kind;
   u2 reference_index;
}
  • tag : 表示常量类型,CONSTANT_MethodHandle_info 的值就是15(CONSTANT_MethodHandle)。
  • reference_kind :表示方法句柄的类型。

    值范围是 0 ~ 9,不同的值决定了句柄字节码的行为。

  • reference_index :就是常量池中的一个有效索引,因为reference_kind 值的不同,分为几种情况:
    • 如果reference_kind 的值是 1 (REF_getField), 2
      (REF_getStatic), 3 (REF_putField), 或者 4 (REF_putStatic);都是和字段相关,那么reference_index 必须是常量池中CONSTANT_Fieldref_info 类型常量的有效索引。
    • 如果reference_kind 的值是5 (REF_invokeVirtual) 或者 8
      (REF_newInvokeSpecial),那么reference_index 必须是常量池中CONSTANT_Methodref_info 类型常量的有效索引,表示一个类中的实例方法或者构造函数。
    • 如果reference_kind 的值是6 (REF_invokeStatic)
      或者 7 (REF_invokeSpecial),且class 版本小于 52 (即 JVM8),那么reference_index必须是CONSTANT_Methodref_info 类型索引;如果 版本大于或者等于 52,那么reference_index必须是CONSTANT_Methodref_info 类型索引或者 CONSTANT_InterfaceMethodref_info 类型索引,因为JVM8 以上接口有默认方法。
    • 如果reference_kind 的值是9 (REF_invokeInterface),那么reference_index必须是CONSTANT_InterfaceMethodref_info 类型索引。
    • 如果reference_kind 的值是5 (REF_invokeVirtual), 6
      (REF_invokeStatic), 7 (REF_invokeSpecial), or 9(REF_invokeInterface),那么reference_index必须是CONSTANT_Methodref_info 类型索引或者 CONSTANT_InterfaceMethodref_info 类型索引,但是不能是实例初始化方法 和 类初始化方法
    • 如果reference_kind 的值是 8 (REF_newInvokeSpecial),那么reference_index必须是CONSTANT_Methodref_info 类型索引,而且它就是实例初始化方法

4.9 CONSTANT_MethodType_info

CONSTANT_MethodType_info {
   u1 tag;
   u2 descriptor_index;
}
  • tag : 表示常量类型,CONSTANT_MethodType_info 的值就是16(CONSTANT_MethodType)。
  • descriptor_index : 表示方法的描述符。

    值必须是当前常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

4.10 CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info {
   u1 tag;
   u2 bootstrap_method_attr_index;
   u2 name_and_type_index;
}
  • tag : 表示常量类型,CONSTANT_InvokeDynamic_info 的值就是18(CONSTANT_InvokeDynamic)。
  • bootstrap_method_attr_index : 表示引导方法 bootstrap_methods 中的有效索引。

    引导方法 bootstrap_methods 记录在 Class 文件BootstrapMethods 属性中。

  • name_and_type_index : 表示方法的名称和描述符的索引。

    值必须是当前常量池中一个 CONSTANT_NameAndType_info 类型常量的有效索引。

五. 访问标志(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 表示为枚举类型

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

  • 内部类中的三个修饰符信息,没有出现在Class 文件 中类的访问标志里。

    的确是这样,三个修饰符只会控制 javac 编译行为的不同,编译完的内部类 class 文件中,是没有这三个修饰符信息。例如,非 static 修饰的内部类, javac 编译的时候,会在 class 文件中,添加一个字段,类型就是外部类。

  • ACC_INTERFACE,ACC_ANNOTATIONACC_ENUM 分别表示 java 目前的三种类型 interface,@interfaceenum

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];
}
  • access_flags : 表示字段访问标志,在上面已经介绍了。
  • name_index : 表示字段的名称。

    值必须是常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

  • descriptor_index : 表示字段的描述符。

    值必须是常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

  • attributes_count : 表示当前字段的属性数量。
  • 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];
}
  • access_flags : 表示方法访问标志,在上面已经介绍了。
  • name_index : 表示方法的名称。

    值必须是常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

  • descriptor_index : 表示方法的描述符。

    值必须是常量池中一个 CONSTANT_Utf8_info 类型常量的有效索引。

  • attributes_count : 表示当前方法的属性数量。
  • 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."":()V
   #2 = Class              #14            // com/zhang/jvm/reflect/example/T
   #3 = Class              #15            // java/lang/Object
   #4 = Utf8               
   #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          // "":()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."":()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默认的构造器方法),最后类属性相关信息(就是这里的SourceFile: "T.java")。
  • this_class,super_class,interfaces 相关信息就在 public class com.zhang.jvm.reflect.example.T 中,Object 父类就隐藏。

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

  • #1 : 这是一个 CONSTANT_Methodref_info 类型,值是java/lang/Object."":()V

    很明显它就是 Object 类构造器方法的符号引用,通过这个引用来调用Object 类构造器方法。

  • #2 : 这是一个 CONSTANT_Class_info 类型,值是 com/zhang/jvm/reflect/example/T

    就是本类的全限定名称。

  • #3 : 这是一个 CONSTANT_Class_info 类型,值是 java/lang/Object
  • #4 : 这是一个 CONSTANT_Utf8_info 类型,值是

    默认构造器方法的名称。

  • #5 : 这是一个 CONSTANT_Utf8_info 类型,值是 ()V

    默认构造器方法的描述符。

  • #6 : 这是一个 CONSTANT_Utf8_info 类型,值是 Code,是一个属性名称。
  • #7 : 这是一个 CONSTANT_Utf8_info 类型,值是 LineNumberTable,是一个属性名称。
  • #8 : 这是一个 CONSTANT_Utf8_info 类型,值是 LocalVariableTable,是一个属性名称。
  • #9 : 这是一个 CONSTANT_Utf8_info 类型,值是 this
  • #10 : 这是一个 CONSTANT_Utf8_info 类型,值是 Lcom/zhang/jvm/reflect/example/T;

    它是T 类型的描述符。

  • #11 : 这是一个 CONSTANT_Utf8_info 类型,值是 SourceFile,是一个属性名称。
  • #12 : 这是一个 CONSTANT_Utf8_info 类型,值是 T.java
  • #13 : 这是一个 CONSTANT_Utf8_info 类型,值是 "":()V

    它是构造器方法的描述符。

  • #14 : 这是一个 CONSTANT_Utf8_info 类型,值是 com/zhang/jvm/reflect/example/T

    就是本类的全限定名称。

  • #14 : 这是一个 CONSTANT_Utf8_info 类型,值是 java/lang/Object

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."":()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               
   #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          // "":()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."":()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."":()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               
  #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         // "":()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."":()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."":()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               
   #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          // "":()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."":()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."":()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               
   #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          // "":()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."":()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."":()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."":()V
   #9 = Class              #47            // java/lang/StringBuilder
  #10 = Methodref          #9.#38         // java/lang/StringBuilder."":()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               
  #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        // "":()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."":()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 "":()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."":()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."":()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               
  #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        // "":()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."":()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 命令看到,我就简单地说一下,有如下重点:

  • TInner 字节码中有两个字段,namethis$0

    this$0 的类型就是 Lcom/zhang/jvm/reflect/example/TS; ,也就是说内部类都持有一个外部类类型的字段。

  • TInner 字节码的构造器方法
    0 aload_0
    1 aload_1
    2 putfield #2 
    5 aload_0
    6 invokespecial #3 >
    9 return
    

    也就是说内部类的构造函数肯定会将外部类的引用传递进来的,然后赋值给this$0字段。

  • 如果外部类给内部类的私有变量赋值了,那么就会生成一个静态方法,来进行内部类私有变量的赋值。

你可能感兴趣的:(JVM_字节码文件(ClassFile)详解)